diff --git a/.rebase/CHANGELOG.md b/.rebase/CHANGELOG.md index f3436192e48..c2db95a4411 100644 --- a/.rebase/CHANGELOG.md +++ b/.rebase/CHANGELOG.md @@ -8,14 +8,6 @@ https://github.com/che-incubator/che-code/pull/549 - code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts --- -#### @RomanNikitenko -https://github.com/che-incubator/che-code/pull/546 - -- code/package.json -- code/build/package.json -- code/remote/package.json ---- - #### @RomanNikitenko https://github.com/che-incubator/che-code/pull/545 @@ -25,13 +17,11 @@ https://github.com/che-incubator/che-code/pull/545 #### @RomanNikitenko https://github.com/che-incubator/che-code/pull/540 \ https://github.com/RomanNikitenko/che-code/commit/724c0a97f73e070f80818091a8d19b7ed186b394 \ -https://github.com/RomanNikitenko/che-code/commit/1e51134551f4c876c4d6de388dcab90180d4607d \ -https://github.com/RomanNikitenko/che-code/commit/7ea82ac755a21d2cb021736a07d8216fe294beff +https://github.com/RomanNikitenko/che-code/commit/1e51134551f4c876c4d6de388dcab90180d4607d - code/package.json - code/remote/package.json - code/src/vs/platform/extensionManagement/node/extensionManagementService.ts -- code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts --- #### @RomanNikitenko diff --git a/.rebase/add/code/build/package.json b/.rebase/add/code/build/package.json deleted file mode 100644 index f1300f6fcdf..00000000000 --- a/.rebase/add/code/build/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "overrides": { - "prebuild-install": { - "tar-fs": "2.1.2" - } - } -} diff --git a/.rebase/add/code/package.json b/.rebase/add/code/package.json index c87b5d34be2..f8695bb830c 100644 --- a/.rebase/add/code/package.json +++ b/.rebase/add/code/package.json @@ -16,12 +16,6 @@ }, "postcss": { "nanoid": "3.3.8" - }, - "@vscode/test-web": { - "tar-fs": "3.0.8" - }, - "prebuild-install": { - "tar-fs": "2.1.2" } } } diff --git a/.rebase/add/code/remote/package.json b/.rebase/add/code/remote/package.json index 7dea0cfcb9a..a3ff54fd45a 100644 --- a/.rebase/add/code/remote/package.json +++ b/.rebase/add/code/remote/package.json @@ -3,10 +3,5 @@ "ws": "8.2.3", "js-yaml": "^4.1.0", "@kubernetes/client-node": "^0.22.0" - }, - "overrides": { - "prebuild-install": { - "tar-fs": "2.1.2" - } } } diff --git a/.rebase/override/code/package.json b/.rebase/override/code/package.json index 616816c9aa0..cf1fdbd2d55 100644 --- a/.rebase/override/code/package.json +++ b/.rebase/override/code/package.json @@ -6,7 +6,6 @@ }, "devDependencies": { "@vscode/l10n-dev": "0.0.18", - "electron": "30.5.1", - "typescript": "5.6.0-dev.20240715" + "electron": "30.5.1" } } \ No newline at end of file diff --git a/.rebase/replace/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts.json b/.rebase/replace/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts.json deleted file mode 100644 index 9eae18d5b1f..00000000000 --- a/.rebase/replace/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "from": "const content = VSBuffer.wrap(await response.bytes());", - "by": "const content = VSBuffer.wrap(new Uint8Array(await response.arrayBuffer()));" - } -] diff --git a/.rebase/replace/code/test/automation/src/playwrightBrowser.ts.json b/.rebase/replace/code/test/automation/src/playwrightBrowser.ts.json deleted file mode 100644 index 6e61df7b9be..00000000000 --- a/.rebase/replace/code/test/automation/src/playwrightBrowser.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "from": "headless: headless ?? false,", - "by": "headless: headless ?? false,\\\n\\\t\\\targs: ['--headless=new']," - } -] diff --git a/.rebase/replace/code/test/integration/browser/src/index.ts.json b/.rebase/replace/code/test/integration/browser/src/index.ts.json deleted file mode 100644 index 360590afa7c..00000000000 --- a/.rebase/replace/code/test/integration/browser/src/index.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "from": "const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug) });", - "by": "const browser = await playwright[browserType].launch({ headless: !Boolean(args.debug), args: ['--headless=new'], });" - } -] diff --git a/build/artifacts/artifacts.lock.yaml b/build/artifacts/artifacts.lock.yaml index a73474bab6a..803415ed0bf 100644 --- a/build/artifacts/artifacts.lock.yaml +++ b/build/artifacts/artifacts.lock.yaml @@ -7,9 +7,9 @@ artifacts: filename: ms-vscode.js-debug-companion.1.1.3.vsix checksum: sha256:7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93 # ms-vscode.js-debug - - download_url: https://open-vsx.org/api/ms-vscode/js-debug/1.97.1/file/ms-vscode.js-debug-1.97.1.vsix - filename: ms-vscode.js-debug.1.97.1.vsix - checksum: sha256:977dd854805547702e312e176f68a1b142fa123f228258f47f0964560ad32496 + - download_url: https://open-vsx.org/api/ms-vscode/js-debug/1.100.1/file/ms-vscode.js-debug-1.100.1.vsix + filename: ms-vscode.js-debug.1.100.1.vsix + checksum: sha256:8c2218df3422d45b95e96d9d28cdc4aa4426a2799aaaedd862d3f60ecab03844 # ms-vscode.vscode-js-profile-table - download_url: https://open-vsx.org/api/ms-vscode/vscode-js-profile-table/1.0.10/file/ms-vscode.vscode-js-profile-table-1.0.10.vsix filename: ms-vscode.vscode-js-profile-table.1.0.10.vsix diff --git a/build/dockerfiles/assembly.Dockerfile b/build/dockerfiles/assembly.Dockerfile index 57f17cec5f9..a384aa4f8f4 100644 --- a/build/dockerfiles/assembly.Dockerfile +++ b/build/dockerfiles/assembly.Dockerfile @@ -15,7 +15,7 @@ FROM linux-musl as linux-musl-content FROM quay.io/eclipse/che-machine-exec:7.56.0 as machine-exec # https://registry.access.redhat.com/ubi8/ubi -FROM registry.access.redhat.com/ubi8/ubi:8.9 AS ubi-builder +FROM registry.access.redhat.com/ubi8/ubi:8.10 AS ubi-builder RUN mkdir -p /mnt/rootfs RUN yum install --installroot /mnt/rootfs brotli libstdc++ coreutils glibc-minimal-langpack --releasever 8 --setopt install_weak_deps=false --nodocs -y && yum --installroot /mnt/rootfs clean all RUN rm -rf /mnt/rootfs/var/cache/* /mnt/rootfs/var/log/dnf* /mnt/rootfs/var/log/yum.* diff --git a/build/dockerfiles/dev.Dockerfile b/build/dockerfiles/dev.Dockerfile index 3916f871832..7c1e9b82ee0 100644 --- a/build/dockerfiles/dev.Dockerfile +++ b/build/dockerfiles/dev.Dockerfile @@ -34,7 +34,7 @@ ENV ZSH_DISABLE_COMPFIX="true" USER 10001 -ENV NODEJS_VERSION=20.18.2 +ENV NODEJS_VERSION=20.19.1 ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 \ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0 \ diff --git a/build/dockerfiles/linux-libc-ubi8.Dockerfile b/build/dockerfiles/linux-libc-ubi8.Dockerfile index cabe05a6fff..e8e001950bc 100644 --- a/build/dockerfiles/linux-libc-ubi8.Dockerfile +++ b/build/dockerfiles/linux-libc-ubi8.Dockerfile @@ -7,7 +7,7 @@ # # https://registry.access.redhat.com/ubi8/nodejs-20 -FROM registry.access.redhat.com/ubi8/nodejs-20:1-73.1742991506 as linux-libc-ubi8-builder +FROM registry.access.redhat.com/ubi8/nodejs-20:1-75.1749482737 as linux-libc-ubi8-builder USER root diff --git a/build/dockerfiles/linux-libc-ubi9.Dockerfile b/build/dockerfiles/linux-libc-ubi9.Dockerfile index d97743228d3..dfe4e44a58e 100644 --- a/build/dockerfiles/linux-libc-ubi9.Dockerfile +++ b/build/dockerfiles/linux-libc-ubi9.Dockerfile @@ -7,7 +7,7 @@ # # https://registry.access.redhat.com/ubi9/nodejs-20 -FROM registry.access.redhat.com/ubi9/nodejs-20:9.5-1743584090 as linux-libc-ubi9-builder +FROM registry.access.redhat.com/ubi9/nodejs-20:9.6-1749604222 as linux-libc-ubi9-builder USER root diff --git a/build/dockerfiles/linux-musl.Dockerfile b/build/dockerfiles/linux-musl.Dockerfile index ae89e6e7791..c245f1c583e 100644 --- a/build/dockerfiles/linux-musl.Dockerfile +++ b/build/dockerfiles/linux-musl.Dockerfile @@ -93,11 +93,12 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \ npm run playwright-install; \ fi RUN if [ "$(uname -m)" = "x86_64" ]; then \ - PLAYWRIGHT_CHROMIUM_PATH=$(echo /root/.cache/ms-playwright/chromium-*/) && \ - rm "${PLAYWRIGHT_CHROMIUM_PATH}/chrome-linux/chrome" && \ - ln -s /usr/bin/chromium-browser "${PLAYWRIGHT_CHROMIUM_PATH}/chrome-linux/chrome" && \ - ls -la /checode-compilation/extensions/vscode-api-tests/ && \ - ls -la /checode-compilation/extensions/vscode-api-tests/out/; \ + PLAYWRIGHT_HEADLESS_PATH=$(echo /root/.cache/ms-playwright/chromium_headless_shell-*/chrome-linux) && \ + echo "Found headless_shell path: $PLAYWRIGHT_HEADLESS_PATH" && \ + rm -f "$PLAYWRIGHT_HEADLESS_PATH/headless_shell" && \ + ln -sf /usr/bin/chromium-browser "$PLAYWRIGHT_HEADLESS_PATH/headless_shell" && \ + ln -sf /usr/bin/chromium-browser "$PLAYWRIGHT_HEADLESS_PATH/chrome" && \ + ls -la "$PLAYWRIGHT_HEADLESS_PATH"; \ fi # Run integration tests (Browser) diff --git a/code/.configurations/configuration.dsc.yaml b/code/.config/configuration.winget similarity index 78% rename from code/.configurations/configuration.dsc.yaml rename to code/.config/configuration.winget index c41f115aff9..a9e45597205 100644 --- a/code/.configurations/configuration.dsc.yaml +++ b/code/.config/configuration.winget @@ -5,7 +5,8 @@ properties: - resource: Microsoft.WinGet.DSC/WinGetPackage directives: description: Install Git - allowPrerelease: true + # Requires elevation for the set operation (i.e., for installing the package) + securityContext: elevated settings: id: Git.Git source: winget @@ -13,7 +14,8 @@ properties: id: npm directives: description: Install NodeJS version 20 - allowPrerelease: true + # Requires elevation for the set operation (i.e., for installing the package) + securityContext: elevated settings: id: OpenJS.NodeJS.LTS version: "20.14.0" @@ -21,7 +23,6 @@ properties: - resource: Microsoft.WinGet.DSC/WinGetPackage directives: description: Install Python 3.10 - allowPrerelease: true settings: id: Python.Python.3.10 source: winget @@ -29,7 +30,8 @@ properties: id: vsPackage directives: description: Install Visual Studio 2022 (any edition is OK) - allowPrerelease: true + # Requires elevation for the set operation (i.e., for installing the package) + securityContext: elevated settings: id: Microsoft.VisualStudio.2022.BuildTools source: winget @@ -38,6 +40,8 @@ properties: - vsPackage directives: description: Install required VS workloads + # Requires elevation for the get and set operations + securityContext: elevated allowPrerelease: true settings: productId: Microsoft.VisualStudio.Product.BuildTools diff --git a/code/.eslint-plugin-local/code-no-deep-import-of-internal.ts b/code/.eslint-plugin-local/code-no-deep-import-of-internal.ts new file mode 100644 index 00000000000..3f54665b49a --- /dev/null +++ b/code/.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/code/.github/classifier.json b/code/.github/classifier.json index 93ca6f3293c..c941f85491e 100644 --- a/code/.github/classifier.json +++ b/code/.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/code/.github/workflows/ci.yml b/code/.github/workflows/ci.yml index 813b8dac765..40dca0aaefb 100644 --- a/code/.github/workflows/ci.yml +++ b/code/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: - name: Setup Build Environment run: | sudo apt-get update - sudo apt-get install -y libxkbfile-dev pkg-config libkrb5-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 + sudo apt-get install -y libxkbfile-dev pkg-config libkrb5-dev libxss1 xvfb libgtk-3-0 libgbm1 sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults diff --git a/code/.nvmrc b/code/.nvmrc index 0254b1e633c..5bd6811705e 100644 --- a/code/.nvmrc +++ b/code/.nvmrc @@ -1 +1 @@ -20.18.2 +20.19.0 diff --git a/code/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json b/code/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json index a71a68e4e36..342512be8a9 100644 --- a/code/.vscode/extensions/vscode-selfhost-test-provider/package-lock.json +++ b/code/.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/code/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/code/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts index 17e65cbce50..b211cff4419 100644 --- a/code/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts +++ b/code/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -86,10 +86,10 @@ const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const formatJsonValue = (value: unknown) => { if (typeof value !== 'object') { - return JSON.stringify(value); + return JSON.stringify(value, undefined, '\t'); } - const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); + const src = ts.createSourceFile('', `(${JSON.stringify(value, undefined, '\t')})`, ts.ScriptTarget.ES5, true); const outerExpression = src.statements[0] as ts.ExpressionStatement; const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; diff --git a/code/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/code/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index b5ffd440b33..165855ae6eb 100644 --- a/code/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/code/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -306,9 +306,7 @@ export class DarwinTestRunner extends PosixTestRunner { protected override getDefaultArgs() { return [ TEST_ELECTRON_SCRIPT_PATH, - '--no-sandbox', - '--disable-dev-shm-usage', - '--use-gl=swiftshader', + '--no-sandbox' ]; } diff --git a/code/.vscode/notebooks/api.github-issues b/code/.vscode/notebooks/api.github-issues index 3a0eaec922f..07b5f0c1ca0 100644 --- a/code/.vscode/notebooks/api.github-issues +++ b/code/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"April 2025\"" }, { "kind": 1, diff --git a/code/.vscode/notebooks/endgame.github-issues b/code/.vscode/notebooks/endgame.github-issues index d778179a15d..4911987e921 100644 --- a/code/.vscode/notebooks/endgame.github-issues +++ b/code/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"March 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2025\"" }, { "kind": 1, diff --git a/code/.vscode/notebooks/my-endgame.github-issues b/code/.vscode/notebooks/my-endgame.github-issues index c9890631658..f6e6cc35b4f 100644 --- a/code/.vscode/notebooks/my-endgame.github-issues +++ b/code/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"March 2025\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"April 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/code/.vscode/settings.json b/code/.vscode/settings.json index 80c37853fe7..ba659d5ac8b 100644 --- a/code/.vscode/settings.json +++ b/code/.vscode/settings.json @@ -168,13 +168,16 @@ "[github-issues]": { "editor.wordWrap": "on" }, + "inlineChat.enableV2": true, "css.format.spaceAroundSelectorSeparator": true, - "typescript.enablePromptUseWorkspaceTsdk": true, "eslint.useFlatConfig": true, "editor.occurrencesHighlightDelay": 0, + // "editor.experimental.preferTreeSitter.typescript": true, + // "editor.experimental.preferTreeSitter.regex": true, + // "editor.experimental.preferTreeSitter.css": true, "typescript.experimental.expandableHover": true, - "git.diagnosticsCommitHook.Enabled": true, - "git.diagnosticsCommitHook.Sources": { + "git.diagnosticsCommitHook.enabled": true, + "git.diagnosticsCommitHook.sources": { "*": "error", "ts": "warning", "eslint": "warning" diff --git a/code/ThirdPartyNotices.txt b/code/ThirdPartyNotices.txt index 6be514a4572..ce3bf61f52c 100644 --- a/code/ThirdPartyNotices.txt +++ b/code/ThirdPartyNotices.txt @@ -1230,7 +1230,7 @@ more details. --------------------------------------------------------- -go-syntax 0.7.9 - MIT +go-syntax 0.8.0 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -1546,7 +1546,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.9.0 - MIT +jlelong/vscode-latex-basics 1.13.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors diff --git a/code/build/.cachesalt b/code/build/.cachesalt index 5a294f35396..2b7a6b46c22 100644 --- a/code/build/.cachesalt +++ b/code/build/.cachesalt @@ -1 +1 @@ -2024-12-11T00:28:56.838Z +2025-04-08T11:12:10.188Z diff --git a/code/build/azure-pipelines/alpine/cli-build-alpine.yml b/code/build/azure-pipelines/alpine/cli-build-alpine.yml index 145133481f2..6c0543d2e7c 100644 --- a/code/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/code/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -67,11 +67,11 @@ steps: VSCODE_CLI_ARTIFACT: vscode_cli_alpine_arm64_cli VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_CLI_ENV: - CXX_aarch64-unknown-linux-musl: musl-g++ - CC_aarch64-unknown-linux-musl: musl-gcc OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-linux-musl/lib OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-linux-musl/include OPENSSL_STATIC: "1" + SYSROOT_ARCH: arm64 + IS_MUSL: "1" - ${{ if eq(parameters.VSCODE_BUILD_ALPINE, true) }}: - template: ../cli/cli-compile.yml@self diff --git a/code/build/azure-pipelines/alpine/product-build-alpine.yml b/code/build/azure-pipelines/alpine/product-build-alpine.yml index d6fe74a9d61..95aa6fa2449 100644 --- a/code/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/code/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/code/build/azure-pipelines/cli/cli-compile.yml b/code/build/azure-pipelines/cli/cli-compile.yml index a5d8bdc1a2c..8c9eec62d53 100644 --- a/code/build/azure-pipelines/cli/cli-compile.yml +++ b/code/build/azure-pipelines/cli/cli-compile.yml @@ -43,14 +43,20 @@ steps: set -e if [ -n "$SYSROOT_ARCH" ]; then export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots - node -e '(async () => { const { getVSCodeSysroot } = require("../build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + node -e '(async () => { const { getVSCodeSysroot } = require("../build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"], process.env["IS_MUSL"] === "1"); })()' if [ "$SYSROOT_ARCH" == "arm64" ]; then - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc" - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export CC_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export PKG_CONFIG_LIBDIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/share/pkgconfig" - export PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export OBJDUMP="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/bin/objdump" + if [ -n "$IS_MUSL" ]; then + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$VSCODE_SYSROOT_DIR/output/bin/aarch64-linux-musl-gcc" + export CC_aarch64_unknown_linux_musl="$VSCODE_SYSROOT_DIR/output/bin/aarch64-linux-musl-gcc" + export CXX_aarch64_unknown_linux_musl="$VSCODE_SYSROOT_DIR/output/bin/aarch64-linux-musl-g++" + else + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc" + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export CC_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc --sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export PKG_CONFIG_LIBDIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig:$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/share/pkgconfig" + export PKG_CONFIG_SYSROOT_DIR_aarch64_unknown_linux_gnu="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export OBJDUMP="$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/bin/objdump" + fi elif [ "$SYSROOT_ARCH" == "amd64" ]; then export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc" export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -C link-arg=-L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu" @@ -71,7 +77,7 @@ steps: cargo build --release --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code # verify glibc requirement - if [ -n "$SYSROOT_ARCH" ]; then + if [ -n "$SYSROOT_ARCH" ] && [ -n "$OBJDUMP" ]; then glibc_version="2.28" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then @@ -85,6 +91,8 @@ steps: if [[ "$glibc_version" != "2.28" ]]; then echo "Error: binary has dependency on GLIBC > 2.28, found $glibc_version" exit 1 + else + echo "Maximum GLIBC version is $glibc_version as expected." fi fi displayName: Compile ${{ parameters.VSCODE_CLI_TARGET }} diff --git a/code/build/azure-pipelines/cli/cli-darwin-sign.yml b/code/build/azure-pipelines/cli/cli-darwin-sign.yml index ba8150651a7..d702b82fc57 100644 --- a/code/build/azure-pipelines/cli/cli-darwin-sign.yml +++ b/code/build/azure-pipelines/cli/cli-darwin-sign.yml @@ -36,12 +36,12 @@ steps: - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign + displayName: ✍️ Codesign - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Notarize + displayName: ✍️ Notarize - ${{ each target in parameters.VSCODE_CLI_ARTIFACTS }}: - script: | diff --git a/code/build/azure-pipelines/cli/cli-win32-sign.yml b/code/build/azure-pipelines/cli/cli-win32-sign.yml index bc711bec4a7..eb85e9e2e06 100644 --- a/code/build/azure-pipelines/cli/cli-win32-sign.yml +++ b/code/build/azure-pipelines/cli/cli-win32-sign.yml @@ -44,7 +44,7 @@ steps: - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows $(Build.ArtifactStagingDirectory)/sign "*.exe" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign + displayName: ✍️ Codesign - ${{ each target in parameters.VSCODE_CLI_ARTIFACTS }}: - powershell: | diff --git a/code/build/azure-pipelines/cli/test.yml b/code/build/azure-pipelines/cli/test.yml index 8b525845548..6e2a1c68a16 100644 --- a/code/build/azure-pipelines/cli/test.yml +++ b/code/build/azure-pipelines/cli/test.yml @@ -7,4 +7,4 @@ steps: - script: cargo test workingDirectory: cli - displayName: Run unit tests + displayName: 🧪 Run unit tests diff --git a/code/build/azure-pipelines/common/checkForArtifact.js b/code/build/azure-pipelines/common/checkForArtifact.js new file mode 100644 index 00000000000..899448f78bd --- /dev/null +++ b/code/build/azure-pipelines/common/checkForArtifact.js @@ -0,0 +1,34 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const publish_1 = require("./publish"); +const retry_1 = require("./retry"); +async function getPipelineArtifacts() { + const result = await (0, publish_1.requestAZDOAPI)('artifacts'); + return result.value.filter(a => !/sbom$/.test(a.name)); +} +async function main([variableName, artifactName]) { + if (!variableName || !artifactName) { + throw new Error(`Usage: node checkForArtifact.js `); + } + try { + const artifacts = await (0, retry_1.retry)(() => getPipelineArtifacts()); + const artifact = artifacts.find(a => a.name === artifactName); + console.log(`##vso[task.setvariable variable=${variableName}]${artifact ? 'true' : 'false'}`); + } + catch (err) { + console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); + console.log(`##vso[task.setvariable variable=${variableName}]false`); + } +} +main(process.argv.slice(2)) + .then(() => { + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); +//# sourceMappingURL=checkForArtifact.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/checkForArtifact.ts b/code/build/azure-pipelines/common/checkForArtifact.ts new file mode 100644 index 00000000000..e0a1a2ce1d3 --- /dev/null +++ b/code/build/azure-pipelines/common/checkForArtifact.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Artifact, requestAZDOAPI } from './publish'; +import { retry } from './retry'; + +async function getPipelineArtifacts(): Promise { + const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts'); + return result.value.filter(a => !/sbom$/.test(a.name)); +} + +async function main([variableName, artifactName]: string[]): Promise { + if (!variableName || !artifactName) { + throw new Error(`Usage: node checkForArtifact.js `); + } + + try { + const artifacts = await retry(() => getPipelineArtifacts()); + const artifact = artifacts.find(a => a.name === artifactName); + console.log(`##vso[task.setvariable variable=${variableName}]${artifact ? 'true' : 'false'}`); + } catch (err) { + console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); + console.log(`##vso[task.setvariable variable=${variableName}]false`); + } +} + +main(process.argv.slice(2)) + .then(() => { + process.exit(0); + }, err => { + console.error(err); + process.exit(1); + }); diff --git a/code/build/azure-pipelines/common/codesign.js b/code/build/azure-pipelines/common/codesign.js new file mode 100644 index 00000000000..e3a8f330dcd --- /dev/null +++ b/code/build/azure-pipelines/common/codesign.js @@ -0,0 +1,30 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.printBanner = printBanner; +exports.streamProcessOutputAndCheckResult = streamProcessOutputAndCheckResult; +exports.spawnCodesignProcess = spawnCodesignProcess; +const zx_1 = require("zx"); +function printBanner(title) { + title = `${title} (${new Date().toISOString()})`; + console.log('\n'); + console.log('#'.repeat(75)); + console.log(`# ${title.padEnd(71)} #`); + console.log('#'.repeat(75)); + console.log('\n'); +} +async function streamProcessOutputAndCheckResult(name, promise) { + const result = await promise.pipe(process.stdout); + if (result.ok) { + console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); + return; + } + throw new Error(`${name} failed: ${result.stderr}`); +} +function spawnCodesignProcess(esrpCliDLLPath, type, folder, glob) { + return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; +} +//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/codesign.ts b/code/build/azure-pipelines/common/codesign.ts new file mode 100644 index 00000000000..9f26b3924b5 --- /dev/null +++ b/code/build/azure-pipelines/common/codesign.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, ProcessPromise } from 'zx'; + +export function printBanner(title: string) { + title = `${title} (${new Date().toISOString()})`; + + console.log('\n'); + console.log('#'.repeat(75)); + console.log(`# ${title.padEnd(71)} #`); + console.log('#'.repeat(75)); + console.log('\n'); +} + +export async function streamProcessOutputAndCheckResult(name: string, promise: ProcessPromise): Promise { + const result = await promise.pipe(process.stdout); + if (result.ok) { + console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); + return; + } + + throw new Error(`${name} failed: ${result.stderr}`); +} + +export function spawnCodesignProcess(esrpCliDLLPath: string, type: 'sign-windows' | 'sign-windows-appx' | 'sign-pgp' | 'sign-darwin' | 'notarize-darwin', folder: string, glob: string): ProcessPromise { + return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; +} diff --git a/code/build/azure-pipelines/common/publish.js b/code/build/azure-pipelines/common/publish.js index 8e052881b2c..d65a4348f9b 100644 --- a/code/build/azure-pipelines/common/publish.js +++ b/code/build/azure-pipelines/common/publish.js @@ -7,6 +7,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.e = e; +exports.requestAZDOAPI = requestAZDOAPI; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const stream_1 = require("stream"); @@ -228,11 +230,11 @@ class ESRPReleaseService { } async getReleaseStatus(releaseId) { const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`; - const res = await fetch(url, { + const res = await (0, retry_1.retry)(() => fetch(url, { headers: { 'Authorization': `Bearer ${this.accessToken}` } - }); + })); if (!res.ok) { const text = await res.text(); throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); @@ -241,11 +243,11 @@ class ESRPReleaseService { } async getReleaseDetails(releaseId) { const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`; - const res = await fetch(url, { + const res = await (0, retry_1.retry)(() => fetch(url, { headers: { 'Authorization': `Bearer ${this.accessToken}` } - }); + })); if (!res.ok) { const text = await res.text(); throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); @@ -317,7 +319,7 @@ async function requestAZDOAPI(path) { const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), 2 * 60 * 1000); try { - const res = await fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal }); + const res = await (0, retry_1.retry)(() => fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal })); if (!res.ok) { throw new Error(`Unexpected status code: ${res.status}`); } @@ -618,10 +620,12 @@ async function main() { if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } + let timeline; + let artifacts; let resultPromise = Promise.resolve([]); const operations = []; while (true) { - const [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); + [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name)); const stagesInProgress = [...stages].filter(s => !stagesCompleted.has(s)); const artifactsInProgress = artifacts.filter(a => processing.has(a.name)); @@ -689,9 +693,22 @@ async function main() { console.error(`[${operations[i].name}]`, result.reason); } } + // Fail the job if any of the artifacts failed to publish if (results.some(r => r.status === 'rejected')) { throw new Error('Some artifacts failed to publish'); } + // Also fail the job if any of the stages did not succeed + let shouldFail = false; + for (const stage of stages) { + const record = timeline.records.find(r => r.name === stage && r.type === 'Stage'); + if (record.result !== 'succeeded' && record.result !== 'succeededWithIssues') { + shouldFail = true; + console.error(`Stage ${stage} did not succeed: ${record.result}`); + } + } + if (shouldFail) { + throw new Error('Some stages did not succeed'); + } console.log(`All ${done.size} artifacts published!`); } if (require.main === module) { diff --git a/code/build/azure-pipelines/common/publish.ts b/code/build/azure-pipelines/common/publish.ts index 37bf5ccc6cd..2b1c15007b3 100644 --- a/code/build/azure-pipelines/common/publish.ts +++ b/code/build/azure-pipelines/common/publish.ts @@ -20,7 +20,7 @@ import { BlobClient, BlobServiceClient, BlockBlobClient, ContainerClient, Contai import jws from 'jws'; import { clearInterval, setInterval } from 'node:timers'; -function e(name: string): string { +export function e(name: string): string { const result = process.env[name]; if (typeof result !== 'string') { @@ -480,11 +480,11 @@ class ESRPReleaseService { private async getReleaseStatus(releaseId: string): Promise { const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`; - const res = await fetch(url, { + const res = await retry(() => fetch(url, { headers: { 'Authorization': `Bearer ${this.accessToken}` } - }); + })); if (!res.ok) { const text = await res.text(); @@ -497,11 +497,11 @@ class ESRPReleaseService { private async getReleaseDetails(releaseId: string): Promise { const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`; - const res = await fetch(url, { + const res = await retry(() => fetch(url, { headers: { 'Authorization': `Bearer ${this.accessToken}` } - }); + })); if (!res.ok) { const text = await res.text(); @@ -583,12 +583,12 @@ const azdoFetchOptions = { } }; -async function requestAZDOAPI(path: string): Promise { +export async function requestAZDOAPI(path: string): Promise { const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), 2 * 60 * 1000); try { - const res = await fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal }); + const res = await retry(() => fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal })); if (!res.ok) { throw new Error(`Unexpected status code: ${res.status}`); @@ -600,7 +600,7 @@ async function requestAZDOAPI(path: string): Promise { } } -interface Artifact { +export interface Artifact { readonly name: string; readonly resource: { readonly downloadUrl: string; @@ -620,6 +620,7 @@ interface Timeline { readonly name: string; readonly type: string; readonly state: string; + readonly result: string; }[]; } @@ -959,11 +960,13 @@ async function main() { if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { stages.add('macOS'); } if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } + let timeline: Timeline; + let artifacts: Artifact[]; let resultPromise = Promise.resolve[]>([]); const operations: { name: string; operation: Promise }[] = []; while (true) { - const [timeline, artifacts] = await Promise.all([retry(() => getPipelineTimeline()), retry(() => getPipelineArtifacts())]); + [timeline, artifacts] = await Promise.all([retry(() => getPipelineTimeline()), retry(() => getPipelineArtifacts())]); const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name)); const stagesInProgress = [...stages].filter(s => !stagesCompleted.has(s)); const artifactsInProgress = artifacts.filter(a => processing.has(a.name)); @@ -1044,10 +1047,27 @@ async function main() { } } + // Fail the job if any of the artifacts failed to publish if (results.some(r => r.status === 'rejected')) { throw new Error('Some artifacts failed to publish'); } + // Also fail the job if any of the stages did not succeed + let shouldFail = false; + + for (const stage of stages) { + const record = timeline.records.find(r => r.name === stage && r.type === 'Stage')!; + + if (record.result !== 'succeeded' && record.result !== 'succeededWithIssues') { + shouldFail = true; + console.error(`Stage ${stage} did not succeed: ${record.result}`); + } + } + + if (shouldFail) { + throw new Error('Some stages did not succeed'); + } + console.log(`All ${done.size} artifacts published!`); } diff --git a/code/build/azure-pipelines/common/waitForArtifacts.js b/code/build/azure-pipelines/common/waitForArtifacts.js new file mode 100644 index 00000000000..b9ffb73962d --- /dev/null +++ b/code/build/azure-pipelines/common/waitForArtifacts.js @@ -0,0 +1,46 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const publish_1 = require("../common/publish"); +const retry_1 = require("../common/retry"); +async function getPipelineArtifacts() { + const result = await (0, publish_1.requestAZDOAPI)('artifacts'); + return result.value.filter(a => !/sbom$/.test(a.name)); +} +async function main(artifacts) { + if (artifacts.length === 0) { + throw new Error(`Usage: node waitForArtifacts.js ...`); + } + // This loop will run for 30 minutes and waits to the x64 and arm64 artifacts + // to be uploaded to the pipeline by the `macOS` and `macOSARM64` jobs. As soon + // as these artifacts are found, the loop completes and the `macOSUnivesrsal` + // job resumes. + for (let index = 0; index < 60; index++) { + try { + console.log(`Waiting for artifacts (${artifacts.join(', ')}) to be uploaded (${index + 1}/60)...`); + const allArtifacts = await (0, retry_1.retry)(() => getPipelineArtifacts()); + console.log(` * Artifacts attached to the pipelines: ${allArtifacts.length > 0 ? allArtifacts.map(a => a.name).join(', ') : 'none'}`); + const foundArtifacts = allArtifacts.filter(a => artifacts.includes(a.name)); + console.log(` * Found artifacts: ${foundArtifacts.length > 0 ? foundArtifacts.map(a => a.name).join(', ') : 'none'}`); + if (foundArtifacts.length === artifacts.length) { + console.log(` * All artifacts were found`); + return; + } + } + catch (err) { + console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); + } + await new Promise(c => setTimeout(c, 30_000)); + } + throw new Error(`ERROR: Artifacts (${artifacts.join(', ')}) were not uploaded within 30 minutes.`); +} +main(process.argv.splice(2)).then(() => { + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); +//# sourceMappingURL=waitForArtifacts.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/common/waitForArtifacts.ts b/code/build/azure-pipelines/common/waitForArtifacts.ts new file mode 100644 index 00000000000..3fed6cd38d2 --- /dev/null +++ b/code/build/azure-pipelines/common/waitForArtifacts.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Artifact, requestAZDOAPI } from '../common/publish'; +import { retry } from '../common/retry'; + +async function getPipelineArtifacts(): Promise { + const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts'); + return result.value.filter(a => !/sbom$/.test(a.name)); +} + +async function main(artifacts: string[]): Promise { + if (artifacts.length === 0) { + throw new Error(`Usage: node waitForArtifacts.js ...`); + } + + // This loop will run for 30 minutes and waits to the x64 and arm64 artifacts + // to be uploaded to the pipeline by the `macOS` and `macOSARM64` jobs. As soon + // as these artifacts are found, the loop completes and the `macOSUnivesrsal` + // job resumes. + for (let index = 0; index < 60; index++) { + try { + console.log(`Waiting for artifacts (${artifacts.join(', ')}) to be uploaded (${index + 1}/60)...`); + const allArtifacts = await retry(() => getPipelineArtifacts()); + console.log(` * Artifacts attached to the pipelines: ${allArtifacts.length > 0 ? allArtifacts.map(a => a.name).join(', ') : 'none'}`); + + const foundArtifacts = allArtifacts.filter(a => artifacts.includes(a.name)); + console.log(` * Found artifacts: ${foundArtifacts.length > 0 ? foundArtifacts.map(a => a.name).join(', ') : 'none'}`); + + if (foundArtifacts.length === artifacts.length) { + console.log(` * All artifacts were found`); + return; + } + } catch (err) { + console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); + } + + await new Promise(c => setTimeout(c, 30_000)); + } + + throw new Error(`ERROR: Artifacts (${artifacts.join(', ')}) were not uploaded within 30 minutes.`); +} + +main(process.argv.splice(2)).then(() => { + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); diff --git a/code/build/azure-pipelines/darwin/codesign.js b/code/build/azure-pipelines/darwin/codesign.js new file mode 100644 index 00000000000..edc3a5f6f80 --- /dev/null +++ b/code/build/azure-pipelines/darwin/codesign.js @@ -0,0 +1,30 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const codesign_1 = require("../common/codesign"); +const publish_1 = require("../common/publish"); +async function main() { + const arch = (0, publish_1.e)('VSCODE_ARCH'); + const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); + const pipelineWorkspace = (0, publish_1.e)('PIPELINE_WORKSPACE'); + const folder = `${pipelineWorkspace}/unsigned_vscode_client_darwin_${arch}_archive`; + const glob = `VSCode-darwin-${arch}.zip`; + // Codesign + (0, codesign_1.printBanner)('Codesign'); + const codeSignTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-darwin', folder, glob); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign', codeSignTask); + // Notarize + (0, codesign_1.printBanner)('Notarize'); + const notarizeTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'notarize-darwin', folder, glob); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Notarize', notarizeTask); +} +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); +//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/darwin/codesign.ts b/code/build/azure-pipelines/darwin/codesign.ts new file mode 100644 index 00000000000..a9de0206d6e --- /dev/null +++ b/code/build/azure-pipelines/darwin/codesign.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; +import { e } from '../common/publish'; + +async function main() { + const arch = e('VSCODE_ARCH'); + const esrpCliDLLPath = e('EsrpCliDllPath'); + const pipelineWorkspace = e('PIPELINE_WORKSPACE'); + + const folder = `${pipelineWorkspace}/unsigned_vscode_client_darwin_${arch}_archive`; + const glob = `VSCode-darwin-${arch}.zip`; + + // Codesign + printBanner('Codesign'); + const codeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', folder, glob); + await streamProcessOutputAndCheckResult('Codesign', codeSignTask); + + // Notarize + printBanner('Notarize'); + const notarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', folder, glob); + await streamProcessOutputAndCheckResult('Notarize', notarizeTask); +} + +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); diff --git a/code/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/code/build/azure-pipelines/darwin/product-build-darwin-sign.yml deleted file mode 100644 index dffb6665d99..00000000000 --- a/code/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ /dev/null @@ -1,81 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - task: UseDotNet@2 - inputs: - version: 6.x - - - task: EsrpCodeSigning@5 - inputs: - UseMSIAuthentication: true - ConnectedServiceName: vscode-esrp - AppRegistrationClientId: $(ESRP_CLIENT_ID) - AppRegistrationTenantId: $(ESRP_TENANT_ID) - AuthAKVName: vscode-esrp - AuthSignCertName: esrp-sign - FolderPath: . - Pattern: noop - displayName: 'Install ESRP Tooling' - - - script: | - # For legacy purposes, arch for x64 is just 'darwin' - case $VSCODE_ARCH in - x64) ASSET_ID="darwin" ;; - arm64) ASSET_ID="darwin-arm64" ;; - universal) ASSET_ID="darwin-universal" ;; - esac - echo "##vso[task.setvariable variable=ASSET_ID]$ASSET_ID" - displayName: Set asset id variable - - - script: | - if [ -z "$(ASSET_ID)" ]; then - echo "ASSET_ID is empty" - exit 1 - else - echo "ASSET_ID is set to $(ASSET_ID)" - fi - displayName: Check ASSET_ID variable - - - download: current - artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive - displayName: Download $(VSCODE_ARCH) artifact - - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign - - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Notarize - - - script: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) - displayName: Extract signed app - - - script: | - set -e - APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)" - APP_NAME="`ls $APP_ROOT | head -n 1`" - APP_PATH="$APP_ROOT/$APP_NAME" - codesign -dv --deep --verbose=4 "$APP_PATH" - "$APP_PATH/Contents/Resources/app/bin/code" --export-default-configuration=.build - displayName: Verify signature - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - - - script: mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-x64.zip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin.zip - displayName: Rename x64 build to its legacy name - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-$(ASSET_ID).zip - artifactName: vscode_client_darwin_$(VSCODE_ARCH)_archive - sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) - sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" - sbomPackageVersion: $(Build.SourceVersion) - displayName: Publish client archive diff --git a/code/build/azure-pipelines/darwin/product-build-darwin-test.yml b/code/build/azure-pipelines/darwin/product-build-darwin-test.yml index 1eb387ae199..c542cacaf19 100644 --- a/code/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/code/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -1,12 +1,17 @@ parameters: - name: VSCODE_QUALITY type: string - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + - name: VSCODE_RUN_REMOTE_TESTS type: boolean + - name: VSCODE_TEST_ARTIFACT_NAME + type: string + - name: PUBLISH_TASK_NAME + type: string + default: PublishPipelineArtifact@0 steps: - script: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" @@ -15,62 +20,78 @@ steps: displayName: Download Electron and Playwright retryCountOnTaskFailure: 3 - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: ./scripts/test.sh --tfs "Unit Tests" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - script: npm run test-node - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: npm run test-browser-no-install -- --browser webkit --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Webkit) + displayName: 🧪 Run unit tests (Browser, Webkit) timeoutInMinutes: 30 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - script: npm run test-node -- --build - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: npm run test-browser-no-install -- --build --browser webkit --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Webkit) + displayName: 🧪 Run unit tests (Browser, Webkit) timeoutInMinutes: 30 - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - npm run gulp \ - compile-extension:configuration-editing \ - compile-extension:css-language-features-server \ - compile-extension:emmet \ - compile-extension:git \ - compile-extension:github-authentication \ - compile-extension:html-language-features-server \ - compile-extension:ipynb \ - compile-extension:notebook-renderers \ - compile-extension:json-language-features-server \ - compile-extension:markdown-language-features \ - compile-extension-media \ - compile-extension:microsoft-authentication \ - compile-extension:typescript-language-features \ - compile-extension:vscode-api-tests \ - compile-extension:vscode-colorize-tests \ - compile-extension:vscode-colorize-perf-tests \ - compile-extension:vscode-test-resolver - displayName: Build integration tests - - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: ./scripts/test-integration --tfs "Integration Tests" - displayName: Run integration tests (Electron) + - script: | + set -e + npm run gulp \ + compile-extension:configuration-editing \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:notebook-renderers \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-colorize-perf-tests \ + compile-extension:vscode-test-resolver + displayName: Build integration tests + + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: + - script: ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: 🧪 Run integration tests (Electron) + timeoutInMinutes: 20 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: + - script: ./scripts/test-web-integration.sh --browser webkit + displayName: 🧪 Run integration tests (Browser, Webkit) + timeoutInMinutes: 20 + + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: + - script: ./scripts/test-remote-integration.sh + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: | # Figure out the full absolute path of the product we just built # including the remote server and configure the integration tests @@ -82,15 +103,17 @@ steps: ./scripts/test-integration.sh --build --tfs "Integration Tests" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) - displayName: Run integration tests (Electron) + displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: ./scripts/test-web-integration.sh --browser webkit env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web - displayName: Run integration tests (Browser, Webkit) + displayName: 🧪 Run integration tests (Browser, Webkit) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) @@ -99,42 +122,45 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) - displayName: Run integration tests (Remote) + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: ps -ef - displayName: Diagnostics before smoke test run - continueOnError: true - condition: succeededOrFailed() + - script: ps -ef + displayName: Diagnostics before smoke test run + continueOnError: true + condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: npm run compile - workingDirectory: test/smoke - displayName: Compile smoke tests + # - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + # - script: npm run compile + # workingDirectory: test/smoke + # displayName: Compile smoke tests - - script: npm run gulp compile-extension-media - displayName: Compile extensions for smoke tests + # - script: npm run gulp compile-extension-media + # displayName: Compile extensions for smoke tests - - script: npm run smoketest-no-compile -- --tracing - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + # - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: + # - script: npm run smoketest-no-compile -- --tracing + # timeoutInMinutes: 20 + # displayName: 🧪 Run smoke tests (Electron) - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" npm run smoketest-no-compile -- --tracing --build "$APP_ROOT/$APP_NAME" timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + displayName: 🧪 Run smoke tests (Electron) + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: npm run smoketest-no-compile -- --web --tracing --headless env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + displayName: 🧪 Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: | set -e npm run gulp compile-extension:vscode-test-resolver @@ -144,57 +170,50 @@ steps: env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) - - - script: ps -ef - displayName: Diagnostics after smoke test run - continueOnError: true - condition: succeededOrFailed() - - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: .build/crashes - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: crash-dump-macos-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: crash-dump-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: crash-dump-macos-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Crash Reports" - continueOnError: true - condition: failed() - - # In order to properly symbolify above crash reports - # (if any), we need the compiled native modules too - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: node_modules - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: node-modules-macos-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: node-modules-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: node-modules-macos-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Node Modules" - continueOnError: true - condition: failed() - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: .build/logs - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: logs-macos-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: logs-macos-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: logs-macos-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Log Files" - continueOnError: true - condition: succeededOrFailed() + displayName: 🧪 Run smoke tests (Remote) + + - script: ps -ef + displayName: Diagnostics after smoke test run + continueOnError: true + condition: succeededOrFailed() + + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build/crashes + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: crash-dump-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: crash-dump-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Crash Reports" + continueOnError: true + condition: failed() + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: node_modules + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: node-modules-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: node-modules-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Node Modules" + continueOnError: true + condition: failed() + + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build/logs + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: logs-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: logs-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Log Files" + continueOnError: true + condition: succeededOrFailed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/code/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/code/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 3bb62e15403..ff88bf759ef 100644 --- a/code/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/code/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -50,6 +50,11 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies + - pwsh: node build/azure-pipelines/common/waitForArtifacts.js unsigned_vscode_client_darwin_x64_archive unsigned_vscode_client_darwin_arm64_archive + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Wait for x64 and arm64 artifacts + - download: current artifact: unsigned_vscode_client_darwin_x64_archive displayName: Download x64 artifact @@ -87,14 +92,60 @@ steps: DEBUG=electron-osx-sign* node build/darwin/sign.js $(agent.builddirectory) displayName: Set Hardened Entitlements - - script: pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip * && popd + - script: | + set -e + mkdir -p $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip * && popd displayName: Archive build + - task: UseDotNet@2 + inputs: + version: 6.x + + - task: EsrpCodeSigning@5 + inputs: + UseMSIAuthentication: true + ConnectedServiceName: vscode-esrp + AppRegistrationClientId: $(ESRP_CLIENT_ID) + AppRegistrationTenantId: $(ESRP_TENANT_ID) + AuthAKVName: vscode-esrp + AuthSignCertName: esrp-sign + FolderPath: . + Pattern: noop + displayName: 'Install ESRP Tooling' + + - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: ✍️ Codesign + + - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: ✍️ Notarize + + - script: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + displayName: Extract signed app + + - script: | + set -e + APP_ROOT="$(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH)" + APP_NAME="`ls $APP_ROOT | head -n 1`" + APP_PATH="$APP_ROOT/$APP_NAME" + codesign -dv --deep --verbose=4 "$APP_PATH" + "$APP_PATH/Contents/Resources/app/bin/code" --export-default-configuration=.build + displayName: Verify signature + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) + + - script: mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-x64.zip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin.zip + displayName: Rename x64 build to its legacy name + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + - task: 1ES.PublishPipelineArtifact@1 inputs: - targetPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip - artifactName: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive - sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) - sbomPackageName: "VS Code macOS $(VSCODE_ARCH) (unsigned)" + targetPath: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-universal.zip + artifactName: vscode_client_darwin_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" sbomPackageVersion: $(Build.SourceVersion) displayName: Publish client archive diff --git a/code/build/azure-pipelines/darwin/product-build-darwin.yml b/code/build/azure-pipelines/darwin/product-build-darwin.yml index bd37d675aa2..a6072c8f4fa 100644 --- a/code/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/code/build/azure-pipelines/darwin/product-build-darwin.yml @@ -1,14 +1,22 @@ parameters: + - name: VSCODE_ARCH + type: string - name: VSCODE_QUALITY type: string - name: VSCODE_CIBUILD type: boolean - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + default: false + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + default: false + - name: VSCODE_RUN_REMOTE_TESTS type: boolean + default: false + - name: VSCODE_TEST_ARTIFACT_NAME + type: string + default: "" steps: - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: @@ -45,7 +53,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 @@ -165,15 +173,7 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Transpile - - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-darwin-test.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} - VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} - - - ${{ elseif and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - task: DownloadPipelineArtifact@2 inputs: artifact: unsigned_vscode_cli_darwin_$(VSCODE_ARCH)_cli @@ -218,26 +218,106 @@ steps: - script: | set -e - ARCHIVE_PATH=".build/darwin/client/VSCode-darwin-$(VSCODE_ARCH).zip" + ARCHIVE_PATH="$(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip" mkdir -p $(dirname $ARCHIVE_PATH) - (cd ../VSCode-darwin-$(VSCODE_ARCH) && zip -Xry $(Build.SourcesDirectory)/$ARCHIVE_PATH *) + (cd ../VSCode-darwin-$(VSCODE_ARCH) && zip -Xry $ARCHIVE_PATH *) echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" condition: and(succeededOrFailed(), eq(variables['BUILT_CLIENT'], 'true')) displayName: Package client - - script: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" - condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) - displayName: Generate artifact prefix + - pwsh: node build/azure-pipelines/common/checkForArtifact.js CLIENT_ARCHIVE_UPLOADED unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Check for client artifact - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: $(CLIENT_PATH) - artifactName: $(ARTIFACT_PREFIX)unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + artifactName: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH) sbomPackageName: "VS Code macOS $(VSCODE_ARCH) (unsigned)" sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeeded(), ne(variables['CLIENT_PATH'], ''), eq(variables['CLIENT_ARCHIVE_UPLOADED'], 'false')) + displayName: Publish client archive (unsigned) + + - task: UseDotNet@2 + inputs: + version: 6.x + + - task: EsrpCodeSigning@5 + inputs: + UseMSIAuthentication: true + ConnectedServiceName: vscode-esrp + AppRegistrationClientId: $(ESRP_CLIENT_ID) + AppRegistrationTenantId: $(ESRP_TENANT_ID) + AuthAKVName: vscode-esrp + AuthSignCertName: esrp-sign + FolderPath: . + Pattern: noop + displayName: 'Install ESRP Tooling' + + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)/_tasks | Select-Object -last 1).FullName + $Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName + echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" + displayName: Find ESRP CLI + + - script: npx deemon --detach --wait node build/azure-pipelines/darwin/codesign.js + env: + EsrpCliDllPath: $(EsrpCliDllPath) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: ✍️ Codesign & Notarize + + - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: + - template: product-build-darwin-test.yml@self + parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + VSCODE_TEST_ARTIFACT_NAME: ${{ parameters.VSCODE_TEST_ARTIFACT_NAME }} + VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} + VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} + VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 + + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: + - script: npx deemon --attach node build/azure-pipelines/darwin/codesign.js + condition: succeededOrFailed() + displayName: "Post-job: ✍️ Codesign & Notarize" + + - script: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + displayName: Extract signed app + + - script: | + set -e + APP_ROOT="$(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH)" + APP_NAME="`ls $APP_ROOT | head -n 1`" + APP_PATH="$APP_ROOT/$APP_NAME" + codesign -dv --deep --verbose=4 "$APP_PATH" + "$APP_PATH/Contents/Resources/app/bin/code" --export-default-configuration=.build + displayName: Verify signature + + - script: mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-x64.zip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin.zip + displayName: Rename x64 build to its legacy name + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + + - task: 1ES.PublishPipelineArtifact@1 + inputs: + ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}: + targetPath: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-arm64.zip + ${{ else }}: + targetPath: $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin.zip + artifactName: vscode_client_darwin_$(VSCODE_ARCH)_archive + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) displayName: Publish client archive + - script: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" + condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) + displayName: Generate artifact prefix + - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: $(SERVER_PATH) diff --git a/code/build/azure-pipelines/linux/build-snap.sh b/code/build/azure-pipelines/linux/build-snap.sh new file mode 100755 index 00000000000..144f41cae86 --- /dev/null +++ b/code/build/azure-pipelines/linux/build-snap.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -e + +# Get snapcraft version +snapcraft --version + +# Make sure we get latest packages +sudo apt-get update +sudo apt-get upgrade -y +sudo apt-get install -y curl apt-transport-https ca-certificates + +# Define variables +SNAP_ROOT="$(pwd)/.build/linux/snap/$VSCODE_ARCH" + +# Create snap package +BUILD_VERSION="$(date +%s)" +SNAP_FILENAME="code-$VSCODE_QUALITY-$VSCODE_ARCH-$BUILD_VERSION.snap" +SNAP_PATH="$SNAP_ROOT/$SNAP_FILENAME" +case $VSCODE_ARCH in + x64) SNAPCRAFT_TARGET_ARGS="" ;; + *) SNAPCRAFT_TARGET_ARGS="--target-arch $VSCODE_ARCH" ;; +esac +(cd $SNAP_ROOT/code-* && sudo --preserve-env snapcraft snap $SNAPCRAFT_TARGET_ARGS --output "$SNAP_PATH") diff --git a/code/build/azure-pipelines/linux/codesign.js b/code/build/azure-pipelines/linux/codesign.js new file mode 100644 index 00000000000..98b97db5666 --- /dev/null +++ b/code/build/azure-pipelines/linux/codesign.js @@ -0,0 +1,29 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const codesign_1 = require("../common/codesign"); +const publish_1 = require("../common/publish"); +async function main() { + const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); + // Start the code sign processes in parallel + // 1. Codesign deb package + // 2. Codesign rpm package + const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); + const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); + // Codesign deb package + (0, codesign_1.printBanner)('Codesign deb package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign deb package', codesignTask1); + // Codesign rpm package + (0, codesign_1.printBanner)('Codesign rpm package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign rpm package', codesignTask2); +} +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); +//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/linux/codesign.ts b/code/build/azure-pipelines/linux/codesign.ts new file mode 100644 index 00000000000..1f74cc21ee9 --- /dev/null +++ b/code/build/azure-pipelines/linux/codesign.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; +import { e } from '../common/publish'; + +async function main() { + const esrpCliDLLPath = e('EsrpCliDllPath'); + + // Start the code sign processes in parallel + // 1. Codesign deb package + // 2. Codesign rpm package + const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); + const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); + + // Codesign deb package + printBanner('Codesign deb package'); + await streamProcessOutputAndCheckResult('Codesign deb package', codesignTask1); + + // Codesign rpm package + printBanner('Codesign rpm package'); + await streamProcessOutputAndCheckResult('Codesign rpm package', codesignTask2); +} + +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); diff --git a/code/build/azure-pipelines/linux/product-build-linux-test.yml b/code/build/azure-pipelines/linux/product-build-linux-test.yml index 6796339c738..7e9325354a3 100644 --- a/code/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/code/build/azure-pipelines/linux/product-build-linux-test.yml @@ -1,12 +1,14 @@ parameters: - name: VSCODE_QUALITY type: string - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + - name: VSCODE_RUN_REMOTE_TESTS type: boolean + - name: VSCODE_TEST_ARTIFACT_NAME + type: string - name: PUBLISH_TASK_NAME type: string default: PublishPipelineArtifact@0 @@ -31,76 +33,82 @@ steps: stat $ELECTRON_ROOT/chrome-sandbox displayName: Change setuid helper binary permission - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: ./scripts/test.sh --tfs "Unit Tests" env: DISPLAY: ":10" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - script: npm run test-node - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Chromium) + displayName: 🧪 Run unit tests (Browser, Chromium) timeoutInMinutes: 15 - - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - script: npm run test-node -- --build - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: npm run test-browser-no-install -- --build --browser chromium --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Chromium) + displayName: 🧪 Run unit tests (Browser, Chromium) timeoutInMinutes: 15 - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - npm run gulp \ - compile-extension:configuration-editing \ - compile-extension:css-language-features-server \ - compile-extension:emmet \ - compile-extension:git \ - compile-extension:github-authentication \ - compile-extension:html-language-features-server \ - compile-extension:ipynb \ - compile-extension:notebook-renderers \ - compile-extension:json-language-features-server \ - compile-extension:markdown-language-features \ - compile-extension-media \ - compile-extension:microsoft-authentication \ - compile-extension:typescript-language-features \ - compile-extension:vscode-api-tests \ - compile-extension:vscode-colorize-tests \ - compile-extension:vscode-colorize-perf-tests \ - compile-extension:vscode-test-resolver - displayName: Build integration tests - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + npm run gulp \ + compile-extension:configuration-editing \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:notebook-renderers \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-colorize-perf-tests \ + compile-extension:vscode-test-resolver + displayName: Build integration tests + + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: ./scripts/test-integration.sh --tfs "Integration Tests" env: DISPLAY: ":10" - displayName: Run integration tests (Electron) + displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: ./scripts/test-web-integration.sh --browser chromium - displayName: Run integration tests (Browser, Chromium) + displayName: 🧪 Run integration tests (Browser, Chromium) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: | # Figure out the full absolute path of the product we just built # including the remote server and configure the integration tests @@ -113,15 +121,17 @@ steps: ./scripts/test-integration.sh --build --tfs "Integration Tests" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - displayName: Run integration tests (Electron) + displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - script: ./scripts/test-web-integration.sh --browser chromium env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web - displayName: Run integration tests (Browser, Chromium) + displayName: 🧪 Run integration tests (Browser, Chromium) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) @@ -131,116 +141,110 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) - displayName: Run integration tests (Remote) + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - ps -ef - cat /proc/sys/fs/inotify/max_user_watches - lsof | wc -l - displayName: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) - continueOnError: true - condition: succeededOrFailed() - - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: npm run compile - workingDirectory: test/smoke - displayName: Compile smoke tests + - script: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + displayName: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + continueOnError: true + condition: succeededOrFailed() - - script: npm run gulp compile-extension:markdown-language-features compile-extension:ipynb compile-extension-media compile-extension:vscode-test-resolver - displayName: Build extensions for smoke tests + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: npm run compile + workingDirectory: test/smoke + displayName: Compile smoke tests - - script: npm run gulp node - displayName: Download node.js for remote smoke tests - retryCountOnTaskFailure: 3 + - script: npm run gulp node + displayName: Download node.js for remote smoke tests + retryCountOnTaskFailure: 3 + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: npm run smoketest-no-compile -- --tracing timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + displayName: 🧪 Run smoke tests (Electron) - - script: npm run smoketest-no-compile -- --web --tracing --headless --electronArgs="--disable-dev-shm-usage" + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: + - script: npm run smoketest-no-compile -- --web --tracing --headless timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + displayName: 🧪 Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: npm run smoketest-no-compile -- --remote --tracing timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) + displayName: 🧪 Run smoke tests (Remote) - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - script: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH)" timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + displayName: 🧪 Run smoke tests (Electron) - - script: npm run smoketest-no-compile -- --web --tracing --headless --electronArgs="--disable-dev-shm-usage" + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: + - script: npm run smoketest-no-compile -- --web --tracing --headless env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + displayName: 🧪 Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - script: | set -e - npm run gulp compile-extension:vscode-test-resolver APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)" \ npm run smoketest-no-compile -- --tracing --remote --build "$APP_PATH" timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) + displayName: 🧪 Run smoke tests (Remote) + + - script: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + displayName: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + continueOnError: true + condition: succeededOrFailed() - - script: | - set -e - ps -ef - cat /proc/sys/fs/inotify/max_user_watches - lsof | wc -l - displayName: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) - continueOnError: true - condition: succeededOrFailed() - - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: .build/crashes - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: crash-dump-linux-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: crash-dump-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: crash-dump-linux-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Crash Reports" - continueOnError: true - condition: failed() - - # In order to properly symbolify above crash reports - # (if any), we need the compiled native modules too - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: node_modules - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: node-modules-linux-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: node-modules-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: node-modules-linux-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Node Modules" - continueOnError: true - condition: failed() - - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: .build/logs - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: logs-linux-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: logs-linux-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: logs-linux-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Log Files" - continueOnError: true - condition: succeededOrFailed() + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build/crashes + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: crash-dump-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: crash-dump-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Crash Reports" + continueOnError: true + condition: failed() + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: node_modules + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: node-modules-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: node-modules-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Node Modules" + continueOnError: true + condition: failed() + + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build/logs + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: logs-linux-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: logs-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Log Files" + continueOnError: true + condition: succeededOrFailed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/code/build/azure-pipelines/linux/product-build-linux.yml b/code/build/azure-pipelines/linux/product-build-linux.yml index b9300b1ccba..96f76f6846a 100644 --- a/code/build/azure-pipelines/linux/product-build-linux.yml +++ b/code/build/azure-pipelines/linux/product-build-linux.yml @@ -1,16 +1,22 @@ parameters: - name: VSCODE_QUALITY type: string + - name: VSCODE_ARCH + type: string - name: VSCODE_CIBUILD type: boolean - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + default: false + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + default: false + - name: VSCODE_RUN_REMOTE_TESTS type: boolean - - name: VSCODE_ARCH + default: false + - name: VSCODE_TEST_ARTIFACT_NAME type: string + default: "" steps: - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: @@ -48,7 +54,6 @@ steps: # Start X server ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ - dbus \ xvfb \ libgtk-3-0 \ libxkbfile-dev \ @@ -59,17 +64,13 @@ steps: sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults sudo service xvfb start - # Start dbus session - sudo mkdir -p /var/run/dbus - DBUS_LAUNCH_RESULT=$(sudo dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address) - echo "##vso[task.setvariable variable=DBUS_SESSION_BUS_ADDRESS]$DBUS_LAUNCH_RESULT" displayName: Setup system services - script: node build/setup-npm-registry.js $NPM_REGISTRY 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 @@ -286,16 +287,6 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Transpile client and extensions - - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-linux-test.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} - VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - script: | set -e @@ -338,14 +329,31 @@ steps: echo "##vso[task.setvariable variable=RPM_PATH]$(ls .build/linux/rpm/*/*.rpm)" displayName: Build rpm package - - script: | - set -e - npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap" - ARCHIVE_PATH=".build/linux/snap-tarball/snap-$(VSCODE_ARCH).tar.gz" - mkdir -p $(dirname $ARCHIVE_PATH) - tar -czf $ARCHIVE_PATH -C .build/linux snap - echo "##vso[task.setvariable variable=SNAP_PATH]$ARCHIVE_PATH" - displayName: Prepare snap package + - ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}: + - task: Docker@1 + inputs: + azureSubscriptionEndpoint: vscode + azureContainerRegistry: vscodehub.azurecr.io + command: login + displayName: Login to Container Registry + + - script: | + set -e + npm run gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap" + sudo -E docker run -e VSCODE_ARCH -e VSCODE_QUALITY -v $(pwd):/work -w /work vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64 /bin/bash -c "./build/azure-pipelines/linux/build-snap.sh" + + SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" + SNAP_EXTRACTED_PATH=$(find $SNAP_ROOT -maxdepth 1 -type d -name 'code-*') + SNAP_PATH=$(find $SNAP_ROOT -maxdepth 1 -type f -name '*.snap') + + # SBOM tool doesn't like recursive symlinks + sudo find $SNAP_EXTRACTED_PATH -type l -delete + + echo "##vso[task.setvariable variable=SNAP_EXTRACTED_PATH]$SNAP_EXTRACTED_PATH" + echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" + env: + VSCODE_ARCH: $(VSCODE_ARCH) + displayName: Build snap package - task: UseDotNet@2 inputs: @@ -363,15 +371,35 @@ steps: Pattern: noop displayName: 'Install ESRP Tooling' - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-pgp .build/linux/deb '*.deb' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign deb + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)/_tasks | Select-Object -last 1).FullName + $Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName + echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" + displayName: Find ESRP CLI - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-pgp .build/linux/rpm '*.rpm' + - script: npx deemon --detach --wait node build/azure-pipelines/linux/codesign.js env: + EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign rpm + displayName: ✍️ Codesign deb & rpm + + - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: + - template: product-build-linux-test.yml@self + parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} + VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} + VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} + VSCODE_TEST_ARTIFACT_NAME: ${{ parameters.VSCODE_TEST_ARTIFACT_NAME }} + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 + + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: + - script: npx deemon --attach node build/azure-pipelines/linux/codesign.js + condition: succeededOrFailed() + displayName: "✍️ Post-job: Codesign deb & rpm" - script: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) @@ -430,7 +458,9 @@ steps: - task: 1ES.PublishPipelineArtifact@1 inputs: targetPath: $(SNAP_PATH) - artifactName: $(ARTIFACT_PREFIX)snap-$(VSCODE_ARCH) - sbomEnabled: false + artifactName: vscode_client_linux_$(VSCODE_ARCH)_snap + sbomBuildDropPath: $(SNAP_EXTRACTED_PATH) + sbomPackageName: "VS Code Linux $(VSCODE_ARCH) SNAP" + sbomPackageVersion: $(Build.SourceVersion) condition: and(succeededOrFailed(), ne(variables['SNAP_PATH'], '')) - displayName: Publish snap pre-package + displayName: Publish snap package diff --git a/code/build/azure-pipelines/linux/snap-build-linux.yml b/code/build/azure-pipelines/linux/snap-build-linux.yml deleted file mode 100644 index 4d0d26411c3..00000000000 --- a/code/build/azure-pipelines/linux/snap-build-linux.yml +++ /dev/null @@ -1,64 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - task: DownloadPipelineArtifact@2 - displayName: "Download Pipeline Artifact" - inputs: - artifact: snap-$(VSCODE_ARCH) - path: .build/linux/snap-tarball - - - script: | - set -e - - # Get snapcraft version - snapcraft --version - - # Make sure we get latest packages - sudo apt-get update - sudo apt-get upgrade -y - sudo apt-get install -y curl apt-transport-https ca-certificates - - # Define variables - SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" - - # Unpack snap tarball artifact, in order to preserve file perms - (cd .build/linux && tar -xzf snap-tarball/snap-$(VSCODE_ARCH).tar.gz) - - # Create snap package - BUILD_VERSION="$(date +%s)" - SNAP_FILENAME="code-$VSCODE_QUALITY-$(VSCODE_ARCH)-$BUILD_VERSION.snap" - SNAP_PATH="$SNAP_ROOT/$SNAP_FILENAME" - case $(VSCODE_ARCH) in - x64) SNAPCRAFT_TARGET_ARGS="" ;; - *) SNAPCRAFT_TARGET_ARGS="--target-arch $(VSCODE_ARCH)" ;; - esac - (cd $SNAP_ROOT/code-* && sudo --preserve-env snapcraft snap $SNAPCRAFT_TARGET_ARGS --output "$SNAP_PATH") - displayName: Prepare for publish - - - script: | - set -e - SNAP_ROOT="$(pwd)/.build/linux/snap/$(VSCODE_ARCH)" - SNAP_EXTRACTED_PATH=$(find $SNAP_ROOT -maxdepth 1 -type d -name 'code-*') - SNAP_PATH=$(find $SNAP_ROOT -maxdepth 1 -type f -name '*.snap') - - # SBOM tool doesn't like recursive symlinks - sudo find $SNAP_EXTRACTED_PATH -type l -delete - - echo "##vso[task.setvariable variable=SNAP_EXTRACTED_PATH]$SNAP_EXTRACTED_PATH" - echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" - target: - container: host - displayName: Find host snap path & prepare for SBOM - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(SNAP_PATH) - artifactName: vscode_client_linux_$(VSCODE_ARCH)_snap - sbomBuildDropPath: $(SNAP_EXTRACTED_PATH) - sbomPackageName: "VS Code Linux $(VSCODE_ARCH) SNAP" - sbomPackageVersion: $(Build.SourceVersion) - displayName: Publish snap package diff --git a/code/build/azure-pipelines/oss/product-build-pr-cache-darwin.yml b/code/build/azure-pipelines/oss/product-build-pr-cache-darwin.yml new file mode 100644 index 00000000000..d382918a6c3 --- /dev/null +++ b/code/build/azure-pipelines/oss/product-build-pr-cache-darwin.yml @@ -0,0 +1,79 @@ +steps: + - checkout: self + fetchDepth: 1 + retryCountOnTaskFailure: 3 + + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + + - script: node build/setup-npm-registry.js $NPM_REGISTRY + 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 $(node -p process.arch) > .build/packagelockhash + displayName: Prepare node_modules cache key + + - task: Cache@2 + inputs: + key: '"node_modules" | .build/packagelockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + set -e + # Set the private NPM registry to the global npmrc file + # so that authentication works for subfolders like build/, remote/, extensions/ etc + # which does not have their own .npmrc file + npm config set registry "$NPM_REGISTRY" + echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM + + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication + + - script: | + set -e + c++ --version + xcode-select -print-path + python3 -m pip install setuptools + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: $(VSCODE_ARCH) + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + # Avoid using dlopen to load Kerberos on macOS which can cause missing libraries + # https://github.com/mongodb-js/kerberos/commit/04044d2814ad1d01e77f1ce87f26b03d86692cf2 + # flipped the default to support legacy linux distros which shouldn't happen + # on macOS. + GYP_DEFINES: "kerberos_use_rtld=false" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - script: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive diff --git a/code/build/azure-pipelines/oss/product-build-pr-cache-linux.yml b/code/build/azure-pipelines/oss/product-build-pr-cache-linux.yml index 72cd33cdd75..b4a2cc3a480 100644 --- a/code/build/azure-pipelines/oss/product-build-pr-cache-linux.yml +++ b/code/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/code/build/azure-pipelines/oss/product-build-pr-cache-win32.yml b/code/build/azure-pipelines/oss/product-build-pr-cache-win32.yml index 76944f69b14..f4a82587567 100644 --- a/code/build/azure-pipelines/oss/product-build-pr-cache-win32.yml +++ b/code/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/code/build/azure-pipelines/product-build-pr.yml b/code/build/azure-pipelines/product-build-pr.yml index 2d66ff3945d..e851856eb12 100644 --- a/code/build/azure-pipelines/product-build-pr.yml +++ b/code/build/azure-pipelines/product-build-pr.yml @@ -22,191 +22,211 @@ variables: - name: VSCODE_STEP_ON_IT value: false -jobs: +stages: - ${{ if ne(variables['VSCODE_CIBUILD'], true) }}: - - job: Compile + - stage: Compile displayName: Compile & Hygiene - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - steps: - - template: product-compile.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - - job: Linuxx64UnitTest - displayName: Linux (Unit Tests) - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml@self - parameters: - VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - job: Linuxx64IntegrationTest - displayName: Linux (Integration Tests) - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml@self - parameters: - VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - - job: Linuxx64SmokeTest - displayName: Linux (Smoke Tests) - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux.yml@self - parameters: - VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true - - - job: LinuxCLI - displayName: Linux (CLI) - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - steps: - - template: cli/test.yml@self - - - job: Windowsx64UnitTests - displayName: Windows (Unit Tests) - pool: 1es-oss-windows-2022-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - steps: - - template: win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - job: Windowsx64IntegrationTests - displayName: Windows (Integration Tests) - pool: 1es-oss-windows-2022-x64 - timeoutInMinutes: 60 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - steps: - - template: win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - # - job: Windowsx64SmokeTests - # displayName: Windows (Smoke Tests) - # pool: 1es-oss-windows-2022-x64 - # timeoutInMinutes: 30 - # variables: - # VSCODE_ARCH: x64 - # NPM_ARCH: x64 - # steps: - # - template: win32/product-build-win32.yml@self - # parameters: - # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - # VSCODE_ARCH: x64 - # VSCODE_RUN_UNIT_TESTS: false - # VSCODE_RUN_INTEGRATION_TESTS: false - # VSCODE_RUN_SMOKE_TESTS: true + dependsOn: [] + jobs: + - job: Compile + displayName: Compile & Hygiene + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + steps: + - template: product-compile.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + + - stage: Test + displayName: Test + dependsOn: [] + jobs: + - job: Linuxx64ElectronTest + displayName: Linux (Electron) + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + + - job: Linuxx64BrowserTest + displayName: Linux (Browser) + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + + - job: Linuxx64RemoteTest + displayName: Linux (Remote) + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true + + - job: LinuxCLI + displayName: Linux (CLI) + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + steps: + - template: cli/test.yml@self + + - job: Windowsx64ElectronTests + displayName: Windows (Electron) + pool: 1es-oss-windows-2022-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: win32/product-build-win32.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + + - job: Windowsx64BrowserTests + displayName: Windows (Browser) + pool: 1es-oss-windows-2022-x64 + timeoutInMinutes: 60 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: win32/product-build-win32.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + + - job: Windowsx64RemoteTests + displayName: Windows (Remote) + pool: 1es-oss-windows-2022-x64 + timeoutInMinutes: 60 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: win32/product-build-win32.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true + + - job: macOSx64ElectronTests + displayName: macOS (Electron) + pool: + vmImage: macOS-14 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: darwin/product-build-darwin.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + + - job: macOSx64BrowserTests + displayName: macOS (Browser) + pool: + vmImage: macOS-14 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: darwin/product-build-darwin.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + + - job: macOSx64RemoteTests + displayName: macOS (Remote) + pool: + vmImage: macOS-14 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: darwin/product-build-darwin.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: Linuxx64MaintainNodeModulesCache - displayName: Linux (Maintain node_modules cache) - pool: 1es-oss-ubuntu-22.04-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - steps: - - template: oss/product-build-pr-cache-linux.yml@self - - - job: Windowsx64MaintainNodeModulesCache - displayName: Windows (Maintain node_modules cache) - pool: 1es-oss-windows-2022-x64 - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: x64 - steps: - - template: oss/product-build-pr-cache-win32.yml@self - - # - job: macOSUnitTest - # displayName: macOS (Unit Tests) - # pool: - # vmImage: macOS-11 - # timeoutInMinutes: 60 - # variables: - # BUILDSECMON_OPT_IN: true - # VSCODE_ARCH: x64 - # steps: - # - template: darwin/product-build-darwin.yml@self - # parameters: - # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - # VSCODE_RUN_UNIT_TESTS: true - # VSCODE_RUN_INTEGRATION_TESTS: false - # VSCODE_RUN_SMOKE_TESTS: false - # - job: macOSIntegrationTest - # displayName: macOS (Integration Tests) - # pool: - # vmImage: macOS-11 - # timeoutInMinutes: 60 - # variables: - # BUILDSECMON_OPT_IN: true - # VSCODE_ARCH: x64 - # steps: - # - template: darwin/product-build-darwin.yml@self - # parameters: - # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - # VSCODE_RUN_UNIT_TESTS: false - # VSCODE_RUN_INTEGRATION_TESTS: true - # VSCODE_RUN_SMOKE_TESTS: false - # - job: macOSSmokeTest - # displayName: macOS (Smoke Tests) - # pool: - # vmImage: macOS-11 - # timeoutInMinutes: 60 - # variables: - # BUILDSECMON_OPT_IN: true - # VSCODE_ARCH: x64 - # steps: - # - template: darwin/product-build-darwin.yml@self - # parameters: - # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - # VSCODE_RUN_UNIT_TESTS: false - # VSCODE_RUN_INTEGRATION_TESTS: false - # VSCODE_RUN_SMOKE_TESTS: true + - stage: NodeModuleCache + jobs: + - job: Linuxx64MaintainNodeModulesCache + displayName: Linux (Maintain node_modules cache) + pool: 1es-oss-ubuntu-22.04-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + steps: + - template: oss/product-build-pr-cache-linux.yml@self + + - job: Windowsx64MaintainNodeModulesCache + displayName: Windows (Maintain node_modules cache) + pool: 1es-oss-windows-2022-x64 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + steps: + - template: oss/product-build-pr-cache-win32.yml@self + + - job: macOSx64MaintainNodeModulesCache + displayName: macOS (Maintain node_modules cache) + pool: + vmImage: macOS-14 + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: x64 + steps: + - template: oss/product-build-pr-cache-darwin.yml@self diff --git a/code/build/azure-pipelines/product-build.yml b/code/build/azure-pipelines/product-build.yml index bf25633b5c8..453849168c8 100644 --- a/code/build/azure-pipelines/product-build.yml +++ b/code/build/azure-pipelines/product-build.yml @@ -185,8 +185,6 @@ extends: sourceAnalysisPool: 1es-windows-2022-x64 createAdoIssuesForJustificationsForDisablement: false containers: - snapcraft: - image: vscodehub.azurecr.io/vscode-linux-build-agent:snapcraft-x64 ubuntu-2004-arm64: image: onebranch.azurecr.io/linux/ubuntu-2004-arm64:latest stages: @@ -197,8 +195,6 @@ extends: pool: name: AcesShared os: macOS - variables: - VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/product-compile.yml@self parameters: @@ -220,7 +216,7 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_LINUX: ${{ parameters.VSCODE_BUILD_LINUX }} - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true))) }}: + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: - job: CLILinuxGnuARM pool: name: 1es-ubuntu-22.04-x64 @@ -230,6 +226,16 @@ extends: parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_LINUX_ARMHF: ${{ parameters.VSCODE_BUILD_LINUX_ARMHF }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: + - job: CLILinuxGnuAarch64 + pool: + name: 1es-ubuntu-22.04-x64 + os: linux + steps: + - template: build/azure-pipelines/linux/cli-build-linux.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_BUILD_LINUX_ARM64: ${{ parameters.VSCODE_BUILD_LINUX_ARM64 }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE, true)) }}: @@ -246,15 +252,8 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }}: - job: CLIAlpineARM64 pool: - name: 1es-mariner-2.0-arm64 + name: 1es-ubuntu-22.04-x64 os: linux - hostArchitecture: arm64 - container: ubuntu-2004-arm64 - templateContext: - authenticatedContainerRegistries: - - registry: onebranch.azurecr.io - tenant: AME - identity: 1ESPipelineIdentity steps: - template: build/azure-pipelines/alpine/cli-build-alpine.yml@self parameters: @@ -342,9 +341,9 @@ extends: os: windows jobs: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: WindowsUnitTests - displayName: Unit Tests - timeoutInMinutes: 60 + - job: WindowsElectronTests + displayName: Electron Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 steps: @@ -353,12 +352,11 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsIntegrationTests - displayName: Integration Tests - timeoutInMinutes: 60 + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + - job: WindowsBrowserTests + displayName: Browser Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 steps: @@ -367,12 +365,11 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsSmokeTests - displayName: Smoke Tests - timeoutInMinutes: 60 + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + - job: WindowsRemoteTests + displayName: Remote Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 steps: @@ -381,9 +378,8 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32, true)) }}: - job: Windows @@ -400,9 +396,9 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: x64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_BROWSER_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_REMOTE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - job: WindowsCLISign timeoutInMinutes: 90 @@ -428,9 +424,6 @@ extends: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_ARCH: arm64 VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: - stage: Linux @@ -443,8 +436,9 @@ extends: os: linux jobs: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: Linuxx64UnitTest - displayName: Unit Tests + - job: Linuxx64ElectronTest + displayName: Electron Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -455,11 +449,11 @@ extends: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64IntegrationTest - displayName: Integration Tests + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + - job: Linuxx64BrowserTest + displayName: Browser Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -470,11 +464,11 @@ extends: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64SmokeTest - displayName: Smoke Tests + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + - job: Linuxx64RemoteTest + displayName: Remote Tests + timeoutInMinutes: 30 variables: VSCODE_ARCH: x64 NPM_ARCH: x64 @@ -485,9 +479,8 @@ extends: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - job: Linuxx64 @@ -502,24 +495,9 @@ extends: VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: - - job: LinuxSnap - dependsOn: - - Linuxx64 - container: snapcraft - variables: - VSCODE_ARCH: x64 - templateContext: - authenticatedContainerRegistries: - - registry: onebranch.azurecr.io - tenant: AME - identity: 1ESPipelineIdentity - steps: - - template: build/azure-pipelines/linux/snap-build-linux.yml@self + VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_BROWSER_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_REMOTE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: - job: LinuxArmhf @@ -532,9 +510,6 @@ extends: VSCODE_ARCH: armhf VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: - job: LinuxArm64 @@ -547,9 +522,6 @@ extends: VSCODE_ARCH: arm64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_ALPINE'], true)) }}: - stage: Alpine @@ -585,91 +557,66 @@ 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 jobs: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: macOSUnitTest - displayName: Unit Tests - timeoutInMinutes: 90 + - job: macOSElectronTest + displayName: Electron Tests + timeoutInMinutes: 30 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: + VSCODE_ARCH: arm64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSIntegrationTest - displayName: Integration Tests - timeoutInMinutes: 90 + VSCODE_TEST_ARTIFACT_NAME: electron + VSCODE_RUN_ELECTRON_TESTS: true + - job: macOSBrowserTest + displayName: Browser Tests + timeoutInMinutes: 30 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: + VSCODE_ARCH: arm64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSSmokeTest - displayName: Smoke Tests - timeoutInMinutes: 90 + VSCODE_TEST_ARTIFACT_NAME: browser + VSCODE_RUN_BROWSER_TESTS: true + - job: macOSRemoteTest + displayName: Remote Tests + timeoutInMinutes: 30 variables: - VSCODE_ARCH: x64 + VSCODE_ARCH: arm64 steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: + VSCODE_ARCH: arm64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + VSCODE_TEST_ARTIFACT_NAME: remote + VSCODE_RUN_REMOTE_TESTS: true - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}: - job: macOS timeoutInMinutes: 90 variables: VSCODE_ARCH: x64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: + VSCODE_ARCH: x64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - ${{ if eq(parameters.VSCODE_STEP_ON_IT, false) }}: - - job: macOSTest - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - job: macOSSign - dependsOn: - - macOS - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self - - job: macOSCLISign + - job: macOSCLI timeoutInMinutes: 90 steps: - template: build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml@self @@ -683,44 +630,26 @@ extends: timeoutInMinutes: 90 variables: VSCODE_ARCH: arm64 + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ steps: - template: build/azure-pipelines/darwin/product-build-darwin.yml@self parameters: + VSCODE_ARCH: arm64 VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - - job: macOSARM64Sign - dependsOn: - - macOSARM64 - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self + VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_BROWSER_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_REMOTE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true)) }}: - job: macOSUniversal - dependsOn: - - macOS - - macOSARM64 timeoutInMinutes: 90 variables: VSCODE_ARCH: universal + BUILDS_API_URL: $(System.CollectionUri)$(System.TeamProject)/_apis/build/builds/$(Build.BuildId)/ steps: - template: build/azure-pipelines/darwin/product-build-darwin-universal.yml@self - - job: macOSUniversalSign - dependsOn: - - macOSUniversal - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: universal - steps: - - template: build/azure-pipelines/darwin/product-build-darwin-sign.yml@self - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WEB'], true)) }}: - stage: Web dependsOn: diff --git a/code/build/azure-pipelines/product-compile.yml b/code/build/azure-pipelines/product-compile.yml index 6096157ad8d..fba31eefcd1 100644 --- a/code/build/azure-pipelines/product-compile.yml +++ b/code/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/code/build/azure-pipelines/product-publish.yml b/code/build/azure-pipelines/product-publish.yml index 1127e5212e9..27d6c2b366b 100644 --- a/code/build/azure-pipelines/product-publish.yml +++ b/code/build/azure-pipelines/product-publish.yml @@ -94,43 +94,3 @@ steps: sbomEnabled: false displayName: Publish the artifacts processed for this stage attempt condition: always() - - - pwsh: | - $ErrorActionPreference = 'Stop' - - # Determine which stages we need to watch - $stages = @( - if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } - if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } - if ($env:VSCODE_BUILD_STAGE_ALPINE -eq 'True') { 'Alpine' } - if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } - if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' } - ) - Write-Host "Stages to check: $stages" - - # Get the timeline and see if it says the other stage completed - $timeline = Invoke-RestMethod "$($env:BUILDS_API_URL)timeline?api-version=6.0" -Headers @{ - Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" - } -MaximumRetryCount 5 -RetryIntervalSec 1 - - $failedStages = @() - foreach ($stage in $stages) { - $didStageFail = $timeline.records | Where-Object { - $_.name -eq $stage -and $_.type -eq 'stage' -and $_.result -ne 'succeeded' -and $_.result -ne 'succeededWithIssues' - } - - if($didStageFail) { - $failedStages += $stage - Write-Host "'$stage' failed!" - Write-Host $didStageFail - } else { - Write-Host "'$stage' did not fail." - } - } - - if ($failedStages.Length) { - throw "Failed stages: $($failedStages -join ', '). This stage will now fail so that it is easier to retry failed jobs." - } - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Determine if stage should succeed diff --git a/code/build/azure-pipelines/web/product-build-web.yml b/code/build/azure-pipelines/web/product-build-web.yml index e0e91c1c589..3f94460dfaf 100644 --- a/code/build/azure-pipelines/web/product-build-web.yml +++ b/code/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/code/build/azure-pipelines/win32/codesign.js b/code/build/azure-pipelines/win32/codesign.js new file mode 100644 index 00000000000..630f9a64ba1 --- /dev/null +++ b/code/build/azure-pipelines/win32/codesign.js @@ -0,0 +1,73 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const zx_1 = require("zx"); +const codesign_1 = require("../common/codesign"); +const publish_1 = require("../common/publish"); +async function main() { + (0, zx_1.usePwsh)(); + const arch = (0, publish_1.e)('VSCODE_ARCH'); + const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); + const codeSigningFolderPath = (0, publish_1.e)('CodeSigningFolderPath'); + // Start the code sign processes in parallel + // 1. Codesign executables and shared libraries + // 2. Codesign Powershell scripts + // 3. Codesign context menu appx package (insiders only) + const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); + const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); + const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' + ? (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') + : undefined; + // Codesign executables and shared libraries + (0, codesign_1.printBanner)('Codesign executables and shared libraries'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign executables and shared libraries', codesignTask1); + // Codesign Powershell scripts + (0, codesign_1.printBanner)('Codesign Powershell scripts'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign Powershell scripts', codesignTask2); + if (codesignTask3) { + // Codesign context menu appx package + (0, codesign_1.printBanner)('Codesign context menu appx package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign context menu appx package', codesignTask3); + } + // Create build artifact directory + await (0, zx_1.$) `New-Item -ItemType Directory -Path .build/win32-${arch} -Force`; + // Package client + if (process.env['BUILT_CLIENT']) { + // Product version + const version = await (0, zx_1.$) `node -p "require('../VSCode-win32-${arch}/resources/app/package.json').version"`; + (0, codesign_1.printBanner)('Package client'); + const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}-${version}.zip`; + await (0, zx_1.$) `7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); + await (0, zx_1.$) `7z.exe l ${clientArchivePath}`.pipe(process.stdout); + } + // Package server + if (process.env['BUILT_SERVER']) { + (0, codesign_1.printBanner)('Package server'); + const serverArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}.zip`; + await (0, zx_1.$) `7z.exe a -tzip ${serverArchivePath} ../vscode-server-win32-${arch}`.pipe(process.stdout); + await (0, zx_1.$) `7z.exe l ${serverArchivePath}`.pipe(process.stdout); + } + // Package server (web) + if (process.env['BUILT_WEB']) { + (0, codesign_1.printBanner)('Package server (web)'); + const webArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}-web.zip`; + await (0, zx_1.$) `7z.exe a -tzip ${webArchivePath} ../vscode-server-win32-${arch}-web`.pipe(process.stdout); + await (0, zx_1.$) `7z.exe l ${webArchivePath}`.pipe(process.stdout); + } + // Sign setup + if (process.env['BUILT_CLIENT']) { + (0, codesign_1.printBanner)('Sign setup packages (system, user)'); + const task = (0, zx_1.$) `npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; + await (0, codesign_1.streamProcessOutputAndCheckResult)('Sign setup packages (system, user)', task); + } +} +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); +//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/code/build/azure-pipelines/win32/codesign.ts b/code/build/azure-pipelines/win32/codesign.ts new file mode 100644 index 00000000000..7e7170709b5 --- /dev/null +++ b/code/build/azure-pipelines/win32/codesign.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, usePwsh } from 'zx'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; +import { e } from '../common/publish'; + +async function main() { + usePwsh(); + + const arch = e('VSCODE_ARCH'); + const esrpCliDLLPath = e('EsrpCliDllPath'); + const codeSigningFolderPath = e('CodeSigningFolderPath'); + + // Start the code sign processes in parallel + // 1. Codesign executables and shared libraries + // 2. Codesign Powershell scripts + // 3. Codesign context menu appx package (insiders only) + const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); + const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); + const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' + ? spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') + : undefined; + + // Codesign executables and shared libraries + printBanner('Codesign executables and shared libraries'); + await streamProcessOutputAndCheckResult('Codesign executables and shared libraries', codesignTask1); + + // Codesign Powershell scripts + printBanner('Codesign Powershell scripts'); + await streamProcessOutputAndCheckResult('Codesign Powershell scripts', codesignTask2); + + if (codesignTask3) { + // Codesign context menu appx package + printBanner('Codesign context menu appx package'); + await streamProcessOutputAndCheckResult('Codesign context menu appx package', codesignTask3); + } + + // Create build artifact directory + await $`New-Item -ItemType Directory -Path .build/win32-${arch} -Force`; + + // Package client + if (process.env['BUILT_CLIENT']) { + // Product version + const version = await $`node -p "require('../VSCode-win32-${arch}/resources/app/package.json').version"`; + + printBanner('Package client'); + const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}-${version}.zip`; + await $`7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); + await $`7z.exe l ${clientArchivePath}`.pipe(process.stdout); + } + + // Package server + if (process.env['BUILT_SERVER']) { + printBanner('Package server'); + const serverArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}.zip`; + await $`7z.exe a -tzip ${serverArchivePath} ../vscode-server-win32-${arch}`.pipe(process.stdout); + await $`7z.exe l ${serverArchivePath}`.pipe(process.stdout); + } + + // Package server (web) + if (process.env['BUILT_WEB']) { + printBanner('Package server (web)'); + const webArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}-web.zip`; + await $`7z.exe a -tzip ${webArchivePath} ../vscode-server-win32-${arch}-web`.pipe(process.stdout); + await $`7z.exe l ${webArchivePath}`.pipe(process.stdout); + } + + // Sign setup + if (process.env['BUILT_CLIENT']) { + printBanner('Sign setup packages (system, user)'); + const task = $`npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; + await streamProcessOutputAndCheckResult('Sign setup packages (system, user)', task); + } +} + +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); diff --git a/code/build/azure-pipelines/win32/product-build-win32-test.yml b/code/build/azure-pipelines/win32/product-build-win32-test.yml index ebc59303455..8af78682146 100644 --- a/code/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/code/build/azure-pipelines/win32/product-build-win32-test.yml @@ -3,12 +3,14 @@ parameters: type: string - name: VSCODE_ARCH type: string - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + - name: VSCODE_RUN_REMOTE_TESTS type: boolean + - name: VSCODE_TEST_ARTIFACT_NAME + type: string - name: PUBLISH_TASK_NAME type: string default: PublishPipelineArtifact@0 @@ -20,73 +22,81 @@ steps: displayName: Download Electron and Playwright retryCountOnTaskFailure: 3 - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: .\scripts\test.bat --tfs "Unit Tests" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - powershell: npm run test-node - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - powershell: node test/unit/browser/index.js --browser chromium --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium) + displayName: 🧪 Run unit tests (Browser, Chromium) timeoutInMinutes: 20 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: .\scripts\test.bat --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) + displayName: 🧪 Run unit tests (Electron) timeoutInMinutes: 15 - powershell: npm run test-node -- --build - displayName: Run unit tests (node.js) + displayName: 🧪 Run unit tests (node.js) timeoutInMinutes: 15 + + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - powershell: npm run test-browser-no-install -- --build --browser chromium --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium) + displayName: 🧪 Run unit tests (Browser, Chromium) timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm run gulp ` - compile-extension:configuration-editing ` - compile-extension:css-language-features-server ` - compile-extension:emmet ` - compile-extension:git ` - compile-extension:github-authentication ` - compile-extension:html-language-features-server ` - compile-extension:ipynb ` - compile-extension:notebook-renderers ` - compile-extension:json-language-features-server ` - compile-extension:markdown-language-features ` - compile-extension-media ` - compile-extension:microsoft-authentication ` - compile-extension:typescript-language-features ` - compile-extension:vscode-api-tests ` - compile-extension:vscode-colorize-tests ` - compile-extension:vscode-colorize-perf-tests ` - compile-extension:vscode-test-resolver ` - } - displayName: Build integration tests - - - powershell: .\build\azure-pipelines\win32\listprocesses.bat - displayName: Diagnostics before integration test runs - continueOnError: true - condition: succeededOrFailed() - - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp ` + compile-extension:configuration-editing ` + compile-extension:css-language-features-server ` + compile-extension:emmet ` + compile-extension:git ` + compile-extension:github-authentication ` + compile-extension:html-language-features-server ` + compile-extension:ipynb ` + compile-extension:notebook-renderers ` + compile-extension:json-language-features-server ` + compile-extension:markdown-language-features ` + compile-extension-media ` + compile-extension:microsoft-authentication ` + compile-extension:typescript-language-features ` + compile-extension:vscode-api-tests ` + compile-extension:vscode-colorize-tests ` + compile-extension:vscode-colorize-perf-tests ` + compile-extension:vscode-test-resolver ` + } + displayName: Build integration tests + + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics before integration test runs + continueOnError: true + condition: succeededOrFailed() + + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: .\scripts\test-integration.bat --tfs "Integration Tests" - displayName: Run integration tests (Electron) + displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - powershell: .\scripts\test-web-integration.bat --browser chromium - displayName: Run integration tests (Browser, Chromium) + displayName: 🧪 Run integration tests (Browser, Chromium) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - powershell: .\scripts\test-remote-integration.bat - displayName: Run integration tests (Remote) + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: | # Figure out the full absolute path of the product we just built # including the remote server and configure the integration tests @@ -99,17 +109,19 @@ steps: $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)" exec { .\scripts\test-integration.bat --build --tfs "Integration Tests" } - displayName: Run integration tests (Electron) + displayName: 🧪 Run integration tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web" exec { .\scripts\test-web-integration.bat --browser firefox } - displayName: Run integration tests (Browser, Firefox) + displayName: 🧪 Run integration tests (Browser, Firefox) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -119,102 +131,94 @@ steps: $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)" exec { .\scripts\test-remote-integration.bat } - displayName: Run integration tests (Remote) + displayName: 🧪 Run integration tests (Remote) timeoutInMinutes: 20 - - powershell: .\build\azure-pipelines\win32\listprocesses.bat - displayName: Diagnostics after integration test runs - continueOnError: true - condition: succeededOrFailed() + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics after integration test runs + continueOnError: true + condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - powershell: .\build\azure-pipelines\win32\listprocesses.bat - displayName: Diagnostics before smoke test run - continueOnError: true - condition: succeededOrFailed() + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics before smoke test run + continueOnError: true + condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - powershell: npm run compile - workingDirectory: test/smoke - displayName: Compile smoke tests + # - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + # - powershell: npm run compile + # workingDirectory: test/smoke + # displayName: Compile smoke tests - - powershell: npm run gulp compile-extension-media - displayName: Build extensions for smoke tests + # - powershell: npm run gulp compile-extension-media + # displayName: Build extensions for smoke tests - - powershell: npm run smoketest-no-compile -- --tracing - displayName: Run smoke tests (Electron) - timeoutInMinutes: 20 + # - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: + # - powershell: npm run smoketest-no-compile -- --tracing + # displayName: 🧪 Run smoke tests (Electron) + # timeoutInMinutes: 20 - - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - ${{ if eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true) }}: - powershell: npm run smoketest-no-compile -- --tracing --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - displayName: Run smoke tests (Electron) + displayName: 🧪 Run smoke tests (Electron) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}: - powershell: npm run smoketest-no-compile -- --web --tracing --headless env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH)-web - displayName: Run smoke tests (Browser, Chromium) - timeoutInMinutes: 20 - - - powershell: npm run gulp compile-extension:vscode-test-resolver - displayName: Compile test resolver extension + displayName: 🧪 Run smoke tests (Browser, Chromium) timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_REMOTE_TESTS, true) }}: - powershell: npm run smoketest-no-compile -- --tracing --remote --build "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)\vscode-server-win32-$(VSCODE_ARCH) - displayName: Run smoke tests (Remote) + displayName: 🧪 Run smoke tests (Remote) timeoutInMinutes: 20 - - powershell: .\build\azure-pipelines\win32\listprocesses.bat - displayName: Diagnostics after smoke test run - continueOnError: true - condition: succeededOrFailed() - - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: .build\crashes - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: crash-dump-windows-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: crash-dump-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Crash Reports" - continueOnError: true - condition: failed() - - # In order to properly symbolify above crash reports - # (if any), we need the compiled native modules too - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: node_modules - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: node-modules-windows-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: node-modules-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Node Modules" - continueOnError: true - condition: failed() - - - task: ${{ parameters.PUBLISH_TASK_NAME }} - inputs: - targetPath: .build\logs - ${{ if and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, false)) }}: - artifactName: logs-windows-$(VSCODE_ARCH)-integration-$(System.JobAttempt) - ${{ elseif and(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, false), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - artifactName: logs-windows-$(VSCODE_ARCH)-smoke-$(System.JobAttempt) - ${{ else }}: - artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - sbomEnabled: false - displayName: "Publish Log Files" - continueOnError: true - condition: succeededOrFailed() + - powershell: .\build\azure-pipelines\win32\listprocesses.bat + displayName: Diagnostics after smoke test run + continueOnError: true + condition: succeededOrFailed() + + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build\crashes + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: crash-dump-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Crash Reports" + continueOnError: true + condition: failed() + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: node_modules + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Node Modules" + continueOnError: true + condition: failed() + + - task: ${{ parameters.PUBLISH_TASK_NAME }} + inputs: + targetPath: .build\logs + ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: + artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + ${{ else }}: + artifactName: logs-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) + sbomEnabled: false + displayName: "Publish Log Files" + continueOnError: true + condition: succeededOrFailed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/code/build/azure-pipelines/win32/product-build-win32.yml b/code/build/azure-pipelines/win32/product-build-win32.yml index b10de41da59..e561d8e09b1 100644 --- a/code/build/azure-pipelines/win32/product-build-win32.yml +++ b/code/build/azure-pipelines/win32/product-build-win32.yml @@ -5,12 +5,18 @@ parameters: type: string - name: VSCODE_CIBUILD type: boolean - - name: VSCODE_RUN_UNIT_TESTS + - name: VSCODE_RUN_ELECTRON_TESTS type: boolean - - name: VSCODE_RUN_INTEGRATION_TESTS + default: false + - name: VSCODE_RUN_BROWSER_TESTS type: boolean - - name: VSCODE_RUN_SMOKE_TESTS + default: false + - name: VSCODE_RUN_REMOTE_TESTS type: boolean + default: false + - name: VSCODE_TEST_ARTIFACT_NAME + type: string + default: "" steps: - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: @@ -57,7 +63,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 @@ -171,17 +177,6 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) - - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - - template: product-build-win32-test.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} - VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} - VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - task: DownloadPipelineArtifact@2 @@ -226,87 +221,59 @@ steps: echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version\net6.0\esrpcli.dll" displayName: Find ESRP CLI - - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows $(CodeSigningFolderPath) '*.dll,*.exe,*.node' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign executables and shared libraries - - - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows-appx $(CodeSigningFolderPath) '*.ps1' + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.js } env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign Powershell scripts + displayName: ✍️ Codesign - - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows-appx $(CodeSigningFolderPath) '*.appx' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Codesign context menu appx package + - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: + - template: product-build-win32-test.yml@self + parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} + VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} + VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} + VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} + VSCODE_TEST_ARTIFACT_NAME: ${{ parameters.VSCODE_TEST_ARTIFACT_NAME }} + ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\resources\app\package.json | ConvertFrom-Json - $Version = $PackageJson.version - echo "##vso[task.setvariable variable=VSCODE_VERSION]$Version" + exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.js } condition: succeededOrFailed() - displayName: Get product version - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $ArchivePath = ".build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$(VSCODE_VERSION).zip" - New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath ..\VSCode-win32-$(VSCODE_ARCH)\* "-xr!CodeSignSummary*.md" } - echo "##vso[task.setvariable variable=CLIENT_PATH]$ArchivePath" - - echo "Listing archive contents" - 7z.exe l $ArchivePath - condition: and(succeededOrFailed(), eq(variables['BUILT_CLIENT'], 'true')) - displayName: Package client - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $ArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip" - New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH) } - echo "##vso[task.setvariable variable=SERVER_PATH]$ArchivePath" - - echo "Listing archive contents" - 7z.exe l $ArchivePath - condition: and(succeededOrFailed(), eq(variables['BUILT_SERVER'], 'true')) - displayName: Package server + displayName: "✍️ Post-job: Codesign" - powershell: | - . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $ArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH)-web.zip" - New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH)-web } - echo "##vso[task.setvariable variable=WEB_PATH]$ArchivePath" - echo "Listing archive contents" - 7z.exe l $ArchivePath - condition: and(succeededOrFailed(), eq(variables['BUILT_WEB'], 'true')) - displayName: Package server (web) + $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\resources\app\package.json | ConvertFrom-Json + $Version = $PackageJson.version - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm exec -- npm-run-all -lp "gulp vscode-win32-$(VSCODE_ARCH)-system-setup -- --sign" "gulp vscode-win32-$(VSCODE_ARCH)-user-setup -- --sign" } + $ClientArchivePath = ".build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$Version.zip" + $ServerArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip" + $WebArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH)-web.zip" - $SystemSetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" - $UserSetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" + $SystemSetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" + $UserSetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" mv .build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe $SystemSetupPath mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $UserSetupPath + echo "##vso[task.setvariable variable=CLIENT_PATH]$ClientArchivePath" + echo "##vso[task.setvariable variable=SERVER_PATH]$ServerArchivePath" + echo "##vso[task.setvariable variable=WEB_PATH]$WebArchivePath" + echo "##vso[task.setvariable variable=SYSTEM_SETUP_PATH]$SystemSetupPath" echo "##vso[task.setvariable variable=USER_SETUP_PATH]$UserSetupPath" - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Build setup packages (system, user) + condition: succeededOrFailed() + displayName: Move setup packages - powershell: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) diff --git a/code/build/checksums/nodejs.txt b/code/build/checksums/nodejs.txt index d394605dda3..efada69028b 100644 --- a/code/build/checksums/nodejs.txt +++ b/code/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -1f15b7ed18a580af31cf32bc126572292d820f547bf55bf9cdce08041a24e1d9 node-v20.18.3-darwin-arm64.tar.gz -ba668f64df9239843fefcef095ee539f5ac5aa1b0fc15a71f1ecca16abedec7a node-v20.18.3-darwin-x64.tar.gz -93a9df19238adfaa289f4784041d03edaf2fdd89fbb247faffca2fe4a1000703 node-v20.18.3-linux-arm64.tar.gz -8a84eb34287db6a273066934d7195e429f57b91686b62fc19497210204a2b3de node-v20.18.3-linux-armv7l.tar.gz -9fc3952da39b20d1fcfdb777b198cc035485afbbb1004b4df93f35245d61151e node-v20.18.3-linux-x64.tar.gz -4258e333f4b95060681d61bffa762542a8068547d3dffebe57c575b38d380dda win-arm64/node.exe -528a9aa64888a2a3ba71c6aea89434dd5ab5cb3caa9f0f31345cf5facf685ab0 win-x64/node.exe +c016cd1975a264a29dc1b07c6fbe60d5df0a0c2beb4113c0450e3d998d1a0d9c node-v20.19.0-darwin-arm64.tar.gz +a8554af97d6491fdbdabe63d3a1cfb9571228d25a3ad9aed2df856facb131b20 node-v20.19.0-darwin-x64.tar.gz +618e4294602b78e97118a39050116b70d088b16197cd3819bba1fc18b473dfc4 node-v20.19.0-linux-arm64.tar.gz +2deb2f333b42fcdeb0d215800b3d2b9af64dd88c1d0b05e67b980398d43c4dce node-v20.19.0-linux-armv7l.tar.gz +8a4dbcdd8bccef3132d21e8543940557e55dcf44f00f0a99ba8a062f4552e722 node-v20.19.0-linux-x64.tar.gz +4ec1ae34fc7c0c65b35ec3688b9dc6d8ad5feca69d5ba45f7d72d559dc850fbb win-arm64/node.exe +6e3a39787e667d50487f7335c85636c2823a53e636d73c2c841d45da4e57906c win-x64/node.exe diff --git a/code/build/checksums/vscode-sysroot.txt b/code/build/checksums/vscode-sysroot.txt index 67182b078ed..5744a5f77d4 100644 --- a/code/build/checksums/vscode-sysroot.txt +++ b/code/build/checksums/vscode-sysroot.txt @@ -1,3 +1,7 @@ -0de422a81683cf9e8cf875dbd1e0c27545ac3c775b2d53015daf3ca2b31d3f15 aarch64-linux-gnu-glibc-2.28.tar.gz -7aea163f7fad8cc50000c86b5108be880121d35e2f55d016ef8c96bbe54129eb arm-rpi-linux-gnueabihf-glibc-2.28.tar.gz -dbb927408393041664a020661f2641c9785741be3d29b050b9dac58980967784 x86_64-linux-gnu-glibc-2.28.tar.gz +3baac81a39b69e0929e4700f4f78f022adefc515010054ec393565657c4fff32 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +b4fb7a62ee7a474cfb11d5fb2b73accd6a8c875a559db81d6dfccd0b4a3da442 aarch64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz +633e88658561ab4643bc5998c88e565a26553b0e97fd07672cb452afb4d9b276 aarch64-linux-musl-gcc-10.3.0.tar.gz +6e251200607ac4c4709ebd08b2dc0d9a353ddcfdb47f43a10c2b4cc4b49920c0 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz +f82c8dacbb9dd85819e4801909eb4e842ac12c899632aa75b4839383a18c7501 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-8.5.0.tar.gz +3122af49c493c5c767c2b0772a41119cbdc9803125a705683445b4066dc88b82 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +84acc5a15566c98ddf80631731d672e0ce9febcf3f2e969101e0dfd7ef2405e3 x86_64-linux-gnu-glibc-2.28-gcc-8.5.0.tar.gz diff --git a/code/build/gulpfile.scan.js b/code/build/gulpfile.scan.js index cbcdddb74bc..aafc64e81c2 100644 --- a/code/build/gulpfile.scan.js +++ b/code/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/code/build/gulpfile.vscode.web.js b/code/build/gulpfile.vscode.web.js index 94e28f0f81e..974ced9834d 100644 --- a/code/build/gulpfile.vscode.web.js +++ b/code/build/gulpfile.vscode.web.js @@ -173,7 +173,7 @@ function packageTask(sourceFolderName, destinationFolderName) { const name = product.nameShort; const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) - .pipe(json({ name, version })); + .pipe(json({ name, version, type: 'module' })); const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); diff --git a/code/build/hygiene.js b/code/build/hygiene.js index 914690422cd..3e0d30eea36 100644 --- a/code/build/hygiene.js +++ b/code/build/hygiene.js @@ -66,7 +66,7 @@ function hygiene(some, linting = true) { } // Please do not add symbols that resemble ASCII letters! // eslint-disable-next-line no-misleading-character-class - const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); + const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); if (m) { console.error( file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` diff --git a/code/build/lib/i18n.resources.json b/code/build/lib/i18n.resources.json index 2b510757855..921137824ee 100644 --- a/code/build/lib/i18n.resources.json +++ b/code/build/lib/i18n.resources.json @@ -330,10 +330,6 @@ "name": "vs/workbench/contrib/accessibilitySignals", "project": "vscode-workbench" }, - { - "name": "vs/workbench/contrib/deprecatedExtensionMigrator", - "project": "vscode-workbench" - }, { "name": "vs/workbench/contrib/bracketPairColorizer2Telemetry", "project": "vscode-workbench" diff --git a/code/build/lib/layersChecker.js b/code/build/lib/layersChecker.js index 9785bb8d379..83bd45e9d3f 100644 --- a/code/build/lib/layersChecker.js +++ b/code/build/lib/layersChecker.js @@ -85,6 +85,14 @@ const CORE_TYPES = [ 'SubtleCrypto', 'JsonWebKey', 'MessageEvent', + // node web types + 'ReadableStream', + 'ReadableStreamReadResult', + 'ReadableStreamGenericReader', + 'ReadableStreamDefaultReader', + 'value', + 'done', + '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/code/build/lib/layersChecker.ts b/code/build/lib/layersChecker.ts index f8aa401a021..677b505ab22 100644 --- a/code/build/lib/layersChecker.ts +++ b/code/build/lib/layersChecker.ts @@ -84,6 +84,15 @@ const CORE_TYPES = [ 'SubtleCrypto', 'JsonWebKey', 'MessageEvent', + + // node web types + 'ReadableStream', + 'ReadableStreamReadResult', + 'ReadableStreamGenericReader', + 'ReadableStreamDefaultReader', + 'value', + 'done', + 'DOMException', ]; // Types that are defined in a common layer but are known to be only diff --git a/code/build/lib/propertyInitOrderChecker.js b/code/build/lib/propertyInitOrderChecker.js index dbca887bc22..c4931788047 100644 --- a/code/build/lib/propertyInitOrderChecker.js +++ b/code/build/lib/propertyInitOrderChecker.js @@ -54,19 +54,12 @@ 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', 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', 'vs/editor/browser/widget/diffEditor/utils.ts', 'vs/editor/browser/observableCodeEditor.ts', @@ -95,10 +88,8 @@ const ignored = new Set([ 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', @@ -114,12 +105,6 @@ const ignored = new Set([ 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', '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', @@ -128,42 +113,17 @@ const ignored = new Set([ 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', '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/code/build/lib/propertyInitOrderChecker.ts b/code/build/lib/propertyInitOrderChecker.ts index dc18213566f..bbc98c6f43f 100644 --- a/code/build/lib/propertyInitOrderChecker.ts +++ b/code/build/lib/propertyInitOrderChecker.ts @@ -23,19 +23,12 @@ 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', 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', 'vs/editor/browser/widget/diffEditor/utils.ts', 'vs/editor/browser/observableCodeEditor.ts', @@ -64,10 +57,8 @@ const ignored = new Set([ 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', - 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', @@ -83,12 +74,6 @@ const ignored = new Set([ 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', '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', @@ -97,42 +82,17 @@ const ignored = new Set([ 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', - 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', '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/code/build/lib/standalone.js b/code/build/lib/standalone.js index fb6c4fcf8fe..732a34228b9 100644 --- a/code/build/lib/standalone.js +++ b/code/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/code/build/lib/standalone.ts b/code/build/lib/standalone.ts index be7d07f76ca..b18908dcb03 100644 --- a/code/build/lib/standalone.ts +++ b/code/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/code/build/lib/stylelint/vscode-known-variables.json b/code/build/lib/stylelint/vscode-known-variables.json index 352b536194f..e9cddd521d1 100644 --- a/code/build/lib/stylelint/vscode-known-variables.json +++ b/code/build/lib/stylelint/vscode-known-variables.json @@ -59,6 +59,8 @@ "--vscode-chat-slashCommandForeground", "--vscode-checkbox-background", "--vscode-checkbox-border", + "--vscode-checkbox-disabled-background", + "--vscode-checkbox-disabled-foreground", "--vscode-checkbox-foreground", "--vscode-checkbox-selectBackground", "--vscode-checkbox-selectBorder", @@ -228,15 +230,18 @@ "--vscode-editorGroupHeader-tabsBackground", "--vscode-editorGroupHeader-tabsBorder", "--vscode-editorGutter-addedBackground", + "--vscode-editorGutter-addedSecondaryBackground", "--vscode-editorGutter-background", "--vscode-editorGutter-commentGlyphForeground", "--vscode-editorGutter-commentRangeForeground", "--vscode-editorGutter-commentUnresolvedGlyphForeground", "--vscode-editorGutter-deletedBackground", + "--vscode-editorGutter-deletedSecondaryBackground", "--vscode-editorGutter-foldingControlForeground", "--vscode-editorGutter-itemBackground", "--vscode-editorGutter-itemGlyphForeground", "--vscode-editorGutter-modifiedBackground", + "--vscode-editorGutter-modifiedSecondaryBackground", "--vscode-editorHint-border", "--vscode-editorHint-foreground", "--vscode-editorHoverWidget-background", @@ -572,6 +577,8 @@ "--vscode-profileBadge-foreground", "--vscode-profiles-sashBorder", "--vscode-progressBar-background", + "--vscode-prompt-frontMatter-background", + "--vscode-prompt-frontMatter-inactiveBackground", "--vscode-quickInput-background", "--vscode-quickInput-foreground", "--vscode-quickInput-list-focusBackground", diff --git a/code/build/lib/tsb/builder.js b/code/build/lib/tsb/builder.js index 84308191361..0da1f5c09e6 100644 --- a/code/build/lib/tsb/builder.js +++ b/code/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/code/build/lib/tsb/builder.ts b/code/build/lib/tsb/builder.ts index 7a1b0e0cbb4..1a68131f86d 100644 --- a/code/build/lib/tsb/builder.ts +++ b/code/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/code/build/linux/debian/install-sysroot.js b/code/build/linux/debian/install-sysroot.js index 16d8d01468f..230fbda4de6 100644 --- a/code/build/linux/debian/install-sysroot.js +++ b/code/build/linux/debian/install-sysroot.js @@ -70,7 +70,7 @@ async function fetchUrl(options, retries = 10, retryDelay = 1000) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30 * 1000); - const version = '20240129-253798'; + const version = '20250407-330404'; try { const response = await fetch(`https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`, { headers: ghApiHeaders, @@ -119,18 +119,24 @@ async function fetchUrl(options, retries = 10, retryDelay = 1000) { throw e; } } -async function getVSCodeSysroot(arch) { +async function getVSCodeSysroot(arch, isMusl = false) { let expectedName; let triple; - const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28'; + const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-8.5.0'; switch (arch) { case 'amd64': expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; triple = 'x86_64-linux-gnu'; break; case 'arm64': - expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; - triple = 'aarch64-linux-gnu'; + if (isMusl) { + expectedName = 'aarch64-linux-musl-gcc-10.3.0.tar.gz'; + triple = 'aarch64-linux-musl'; + } + else { + expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; + triple = 'aarch64-linux-gnu'; + } break; case 'armhf': expectedName = `arm-rpi-linux-gnueabihf${prefix}.tar.gz`; @@ -144,7 +150,10 @@ async function getVSCodeSysroot(arch) { } const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path_1.default.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); const stamp = path_1.default.join(sysroot, '.stamp'); - const result = `${sysroot}/${triple}/${triple}/sysroot`; + let result = `${sysroot}/${triple}/${triple}/sysroot`; + if (isMusl) { + result = `${sysroot}/output/${triple}`; + } if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === expectedName) { return result; } diff --git a/code/build/linux/debian/install-sysroot.ts b/code/build/linux/debian/install-sysroot.ts index aa10e39f95f..23cce9e1002 100644 --- a/code/build/linux/debian/install-sysroot.ts +++ b/code/build/linux/debian/install-sysroot.ts @@ -79,7 +79,7 @@ async function fetchUrl(options: IFetchOptions, retries = 10, retryDelay = 1000) try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30 * 1000); - const version = '20240129-253798'; + const version = '20250407-330404'; try { const response = await fetch(`https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`, { headers: ghApiHeaders, @@ -133,18 +133,23 @@ type SysrootDictEntry = { Tarball: string; }; -export async function getVSCodeSysroot(arch: DebianArchString): Promise { +export async function getVSCodeSysroot(arch: DebianArchString, isMusl: boolean = false): Promise { let expectedName: string; let triple: string; - const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28'; + const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-8.5.0'; switch (arch) { case 'amd64': expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; triple = 'x86_64-linux-gnu'; break; case 'arm64': - expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; - triple = 'aarch64-linux-gnu'; + if (isMusl) { + expectedName = 'aarch64-linux-musl-gcc-10.3.0.tar.gz'; + triple = 'aarch64-linux-musl'; + } else { + expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; + triple = 'aarch64-linux-gnu'; + } break; case 'armhf': expectedName = `arm-rpi-linux-gnueabihf${prefix}.tar.gz`; @@ -158,7 +163,10 @@ export async function getVSCodeSysroot(arch: DebianArchString): Promise } const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path.join(tmpdir(), `vscode-${arch}-sysroot`); const stamp = path.join(sysroot, '.stamp'); - const result = `${sysroot}/${triple}/${triple}/sysroot`; + let result = `${sysroot}/${triple}/${triple}/sysroot`; + if (isMusl) { + result = `${sysroot}/output/${triple}`; + } if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) { return result; } diff --git a/code/build/package-lock.json b/code/build/package-lock.json index dbc3bd5c92c..63c7c1a4af6 100644 --- a/code/build/package-lock.json +++ b/code/build/package-lock.json @@ -60,7 +60,8 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0" + "yauzl": "^2.10.0", + "zx": "8.5.0" }, "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", @@ -1192,12 +1193,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": { @@ -4144,6 +4146,7 @@ "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", @@ -4384,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", @@ -4657,6 +4661,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zx": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.5.0.tgz", + "integrity": "sha512-XS5/oKOQxKNfG2sVO6TQQjZF5RqWGE5QGSUOCZZVTnvYr3RDBTdbX3IFmV9CrnycCAQWcY0hAD3DDUa4RJE4+w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } } } } diff --git a/code/build/package.json b/code/build/package.json index fe43b8f347f..c67e4f1e610 100644 --- a/code/build/package.json +++ b/code/build/package.json @@ -54,7 +54,8 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0" + "yauzl": "^2.10.0", + "zx": "8.5.0" }, "type": "commonjs", "scripts": { @@ -65,10 +66,5 @@ "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" - }, - "overrides": { - "prebuild-install": { - "tar-fs": "2.1.2" - } } } diff --git a/code/build/win32/Cargo.lock b/code/build/win32/Cargo.lock index 0bbdba1bc23..11558ceaf04 100644 --- a/code/build/win32/Cargo.lock +++ b/code/build/win32/Cargo.lock @@ -95,7 +95,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.14.2" +version = "0.15.0" dependencies = [ "byteorder", "crc", diff --git a/code/build/win32/Cargo.toml b/code/build/win32/Cargo.toml index 02f8d0d73b1..0724862e273 100644 --- a/code/build/win32/Cargo.toml +++ b/code/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.14.2" +version = "0.15.0" authors = ["Microsoft "] build = "build.rs" diff --git a/code/build/win32/inno_updater.exe b/code/build/win32/inno_updater.exe index e70d5b64ee3..16854986847 100644 Binary files a/code/build/win32/inno_updater.exe and b/code/build/win32/inno_updater.exe differ diff --git a/code/cglicenses.json b/code/cglicenses.json index 8b8710fb8f5..adffd3d1cb4 100644 --- a/code/cglicenses.json +++ b/code/cglicenses.json @@ -613,5 +613,10 @@ "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", "SOFTWARE" ] + }, + { + // Reason: Repository moved from https://github.com/swsnr/gethostname.rs to https://codeberg.org/swsnr/gethostname.rs + "name": "gethostname", + "fullLicenseTextUri": "https://codeberg.org/swsnr/gethostname.rs/raw/branch/main/LICENSE" } ] diff --git a/code/cgmanifest.json b/code/cgmanifest.json index eb5b37d39a7..6ee72b3f757 100644 --- a/code/cgmanifest.json +++ b/code/cgmanifest.json @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "4819c99baa28bf2c1baf411ba100c467fec3d486" + "commitHash": "bb1a61d8737feff534bb85368dab3b7c554c863d" } }, "isOnlyProductionDependency": true, - "version": "20.18.3" + "version": "20.19.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "f98501308a973e0aee2414315b426e5de2c03a60" + "commitHash": "d0594707ded4d564c95badf5322d5893295da4ed" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.3.2" + "version": "34.5.1" }, { "component": { diff --git a/code/cli/Cargo.lock b/code/cli/Cargo.lock index ff45765a0c1..992e23a4410 100644 --- a/code/cli/Cargo.lock +++ b/code/cli/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -536,9 +536,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1717,9 +1717,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.70" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -1749,9 +1749,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.105" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -2676,9 +2676,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d" dependencies = [ "backtrace", "bytes", @@ -2696,9 +2696,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", diff --git a/code/cli/Cargo.toml b/code/cli/Cargo.toml index 2c87d662e07..f6e2f96dbd4 100644 --- a/code/cli/Cargo.toml +++ b/code/cli/Cargo.toml @@ -16,7 +16,7 @@ futures = "0.3.28" clap = { version = "4.3.0", features = ["derive", "env"] } open = "4.1.0" reqwest = { version = "0.11.22", default-features = false, features = ["json", "stream", "native-tls"] } -tokio = { version = "1.28.2", features = ["full"] } +tokio = { version = "1.38.2", features = ["full"] } tokio-util = { version = "0.7.8", features = ["compat", "codec"] } flate2 = { version = "1.0.26", default-features = false, features = ["zlib"] } zip = { version = "0.6.6", default-features = false, features = ["time", "deflate-zlib"] } @@ -77,7 +77,6 @@ russh-keys = { git = "https://github.com/microsoft/vscode-russh", branch = "main [profile.release] strip = true lto = true -codegen-units = 1 [features] default = [] diff --git a/code/cli/ThirdPartyNotices.txt b/code/cli/ThirdPartyNotices.txt index 3d6131c690a..c00f3842d83 100644 --- a/code/cli/ThirdPartyNotices.txt +++ b/code/cli/ThirdPartyNotices.txt @@ -574,7 +574,7 @@ https://github.com/marshallpierce/rust-base64 The MIT License (MIT) -Copyright (c) 2015 Alice Maz +Copyright (c) 2025 Alice Maz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -693,6 +693,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -737,6 +738,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -1514,6 +1516,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -1555,7 +1558,7 @@ SOFTWARE. --------------------------------------------------------- -crossbeam-channel 0.5.13 - MIT OR Apache-2.0 +crossbeam-channel 0.5.15 - MIT OR Apache-2.0 https://github.com/crossbeam-rs/crossbeam The MIT License (MIT) @@ -2738,7 +2741,7 @@ SOFTWARE. --------------------------------------------------------- gethostname 0.4.3 - Apache-2.0 -https://github.com/swsnr/gethostname.rs +https://codeberg.org/swsnr/gethostname.rs.git Apache License Version 2.0, January 2004 @@ -3198,6 +3201,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -4181,6 +4185,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -4890,8 +4895,7 @@ THE SOFTWARE. miniz_oxide 0.7.3 - MIT OR Zlib OR Apache-2.0 https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide -This library (excluding the miniz C code used for tests) is licensed under the MIT license. The library is based on the miniz C library, of which the parts used are dual-licensed under the [MIT license](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L1) and also the [unlicense](https://github.com/Frommi/miniz_oxide/blob/master/miniz/miniz.c#L577). -The parts of miniz that are not covered by the unlicense is [some Zip64 code](https://github.com/richgel999/miniz/commit/224d207ce8fffb908e156d27478be3afb5d83e6a#diff-edc0e9ccfae3b5324b85b3ec0a53dc74) which is only MIT licensed. This and other Zip functionality in miniz is not part of the miniz_oxidde and miniz_oxide_c_api rust libraries. +This library (excluding the original miniz C code used for tests) is dual licensed under the MIT license and Apache 2.0 license. The library is based on the [miniz][MIT license](https://github.com/richgel999/miniz) C library by Rich Geldreich which is released under the MIT license. --------------------------------------------------------- --------------------------------------------------------- @@ -5399,7 +5403,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl 0.10.70 - Apache-2.0 +openssl 0.10.72 - Apache-2.0 https://github.com/sfackler/rust-openssl Copyright 2011-2017 Google Inc. @@ -5478,7 +5482,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -openssl-sys 0.9.105 - MIT +openssl-sys 0.9.107 - MIT https://github.com/sfackler/rust-openssl The MIT License (MIT) @@ -9623,7 +9627,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a --------------------------------------------------------- -tokio 1.37.0 - MIT +tokio 1.38.2 - MIT https://github.com/tokio-rs/tokio MIT License @@ -9651,7 +9655,7 @@ SOFTWARE. --------------------------------------------------------- -tokio-macros 2.2.0 - MIT +tokio-macros 2.3.0 - MIT https://github.com/tokio-rs/tokio MIT License @@ -11544,33 +11548,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a zbus 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11578,33 +11556,7 @@ DEALINGS IN THE SOFTWARE. zbus_macros 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11612,33 +11564,7 @@ DEALINGS IN THE SOFTWARE. zbus_names 2.6.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11780,6 +11706,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`collectable`]: ./collectable [`cpufeatures`]: ./cpufeatures [`dbl`]: ./dbl +[`digest-io`]: ./digest-io [`hex-literal`]: ./hex-literal [`inout`]: ./inout [`opaque-debug`]: ./opaque-debug @@ -11933,33 +11860,7 @@ licences; see files named LICENSE.*.txt for details. zvariant 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11967,33 +11868,7 @@ DEALINGS IN THE SOFTWARE. zvariant_derive 3.15.2 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -12001,31 +11876,5 @@ DEALINGS IN THE SOFTWARE. zvariant_utils 1.0.1 - MIT https://github.com/dbus2/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/code/eslint.config.js b/code/eslint.config.js index 822da54456b..f1b01d02447 100644 --- a/code/eslint.config.js +++ b/code/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', { @@ -1000,7 +1001,6 @@ export default tseslint.config( { 'target': 'src/vs/workbench/api/~', 'restrictions': [ - '@c4312/eventsource-umd', 'vscode', 'vs/base/~', 'vs/base/parts/*/~', diff --git a/code/extensions/che-api/.deps/EXCLUDED/prod.md b/code/extensions/che-api/.deps/EXCLUDED/prod.md index b002a17707f..ddcbda70c45 100755 --- a/code/extensions/che-api/.deps/EXCLUDED/prod.md +++ b/code/extensions/che-api/.deps/EXCLUDED/prod.md @@ -2,5 +2,4 @@ This file lists dependencies that do not need CQs or auto-detection does not wor | Packages | Resolved CQs | | --- | --- | -| `@devfile/api@2.3.0-1738854228` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1738854228) | | `jsep@1.3.9` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/jsep/1.3.9) | \ No newline at end of file diff --git a/code/extensions/che-api/.deps/prod.md b/code/extensions/che-api/.deps/prod.md index 787672e47bc..42a2bd0a980 100755 --- a/code/extensions/che-api/.deps/prod.md +++ b/code/extensions/che-api/.deps/prod.md @@ -2,7 +2,7 @@ | Packages | License | Resolved CQs | | --- | --- | --- | -| `@devfile/api@2.3.0-1738854228` | | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1738854228) | +| `@devfile/api@2.3.0-1738854228` | Apache-2.0 | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1738854228) | | `@eclipse-che/api@7.93.0` | EPL-2.0 | ecd.che | | `@eclipse-che/workspace-telemetry-client@0.0.1-1685523760` | EPL-2.0 | ecd.che | | `@isaacs/cliui@8.0.2` | ISC | #8260 | @@ -25,7 +25,7 @@ | `ansi-styles@4.3.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/ansi-styles/4.3.0) | | `ansi-styles@6.2.1` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/ansi-styles/6.2.1) | | `argparse@2.0.1` | Python-2.0 | CQ22954 | -| `asn1@0.2.6` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/asn1/0.2.6) | +| `asn1@0.2.6` | MIT | #21125 | | `assert-plus@1.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/assert-plus/1.0.0) | | `asynckit@0.4.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/asynckit/0.4.0) | | `aws-sign2@0.7.0` | Apache-2.0 | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/aws-sign2/0.7.0) | @@ -46,7 +46,7 @@ | `dashdash@1.14.1` | MIT | #14596 | | `delayed-stream@1.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/delayed-stream/1.0.0) | | `eastasianwidth@0.2.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/eastasianwidth/0.2.0) | -| `ecc-jsbn@0.1.2` | | #17389 | +| `ecc-jsbn@0.1.2` | MIT | #17389 | | `emoji-regex@8.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/emoji-regex/8.0.0) | | `emoji-regex@9.2.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/emoji-regex/9.2.2) | | `es6-promise@4.2.8` | MIT | #2898 | diff --git a/code/extensions/che-remote/.deps/EXCLUDED/prod.md b/code/extensions/che-remote/.deps/EXCLUDED/prod.md index d2cf9b13dd4..4db08da0723 100755 --- a/code/extensions/che-remote/.deps/EXCLUDED/prod.md +++ b/code/extensions/che-remote/.deps/EXCLUDED/prod.md @@ -2,3 +2,10 @@ This file lists dependencies that do not need CQs or auto-detection does not wor | Packages | Resolved CQs | | --- | --- | +| `@devfile/api@2.3.0-1747843475` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1747843475) | +| `@inversifyjs/common@1.5.2` | transitive dependency | +| `@inversifyjs/container@1.10.3` | transitive dependency | +| `@inversifyjs/core@5.3.3` | transitive dependency | +| `@inversifyjs/prototype-utils@0.1.2` | transitive dependency | +| `@inversifyjs/reflect-metadata-utils@1.2.0` | transitive dependency | +| `inversify@7.5.4` | transitive dependency | diff --git a/code/extensions/che-remote/.deps/dev.md b/code/extensions/che-remote/.deps/dev.md index d6d120dc512..a030e110638 100755 --- a/code/extensions/che-remote/.deps/dev.md +++ b/code/extensions/che-remote/.deps/dev.md @@ -270,7 +270,6 @@ | `locate-path@6.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/locate-path/6.0.0) | | `lodash.memoize@4.1.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/lodash.memoize/4.1.2) | | `lodash.merge@4.6.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/lodash.merge/4.6.2) | -| `lodash@4.17.21` | CC0-1.0 | #2096 | | `lru-cache@5.1.1` | ISC | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/lru-cache/5.1.1) | | `make-dir@4.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/make-dir/4.0.0) | | `make-error@1.3.6` | ISC | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/make-error/1.3.6) | diff --git a/code/extensions/che-remote/.deps/prod.md b/code/extensions/che-remote/.deps/prod.md index d275c23893a..3b6af98d89d 100755 --- a/code/extensions/che-remote/.deps/prod.md +++ b/code/extensions/che-remote/.deps/prod.md @@ -2,13 +2,19 @@ | Packages | License | Resolved CQs | | --- | --- | --- | -| `@devfile/api@2.3.0-1738342178` | Apache-2.0 | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1738342178) | -| `@eclipse-che/che-devworkspace-generator@7.99.0-next-aef9b62` | EPL-2.0 | ecd.che | +| `@devfile/api@2.3.0-1747843475` | | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@devfile/api/2.3.0-1747843475) | +| `@eclipse-che/che-devworkspace-generator@7.105.0-next-a6fff71` | EPL-2.0 | ecd.che | +| `@inversifyjs/common@1.5.2` | | transitive dependency | +| `@inversifyjs/container@1.10.3` | | transitive dependency | +| `@inversifyjs/core@5.3.3` | | transitive dependency | +| `@inversifyjs/plugin@0.2.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/@inversifyjs/plugin/0.2.0) | +| `@inversifyjs/prototype-utils@0.1.2` | | transitive dependency | +| `@inversifyjs/reflect-metadata-utils@1.2.0` | | transitive dependency | | `@types/node-fetch@2.6.12` | MIT | #11004 | | `@types/node@20.16.14` | MIT | #16051 | | `argparse@2.0.1` | Python-2.0 | CQ22954 | | `asynckit@0.4.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/asynckit/0.4.0) | -| `axios@1.7.7` | MIT | #14871 | +| `axios@1.10.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/axios/1.10.0) | | `call-bind-apply-helpers@1.0.2` | MIT | #17826 | | `combined-stream@1.0.8` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/combined-stream/1.0.8) | | `delayed-stream@1.0.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/delayed-stream/1.0.0) | @@ -21,10 +27,10 @@ | `follow-redirects@1.15.9` | MIT | #10782 | | `form-data@2.5.3` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/form-data/2.5.3) | | `form-data@4.0.1` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/form-data/4.0.1) | -| `form-data@4.0.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/form-data/4.0.2) | +| `form-data@4.0.3` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/form-data/4.0.3) | | `fs-extra@11.2.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/fs-extra/11.2.0) | | `function-bind@1.1.2` | MIT | #11063 | -| `get-intrinsic@1.2.7` | MIT | #8453 | +| `get-intrinsic@1.3.0` | MIT | #19525 | | `get-proto@1.0.1` | MIT | #18351 | | `gopd@1.2.0` | MIT | #17752 | | `graceful-fs@4.2.11` | ISC | #7413 | @@ -32,11 +38,12 @@ | `has-tostringtag@1.0.2` | MIT | #13161 | | `hasown@2.0.2` | MIT | #11097 | | `https@1.0.0` | ISC | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/https/1.0.0) | -| `inversify@6.0.2` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/inversify/6.0.2) | +| `inversify@7.5.4` | | transitive dependency | | `js-yaml@4.1.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/js-yaml/4.1.0) | | `jsonc-parser@3.3.1` | MIT | #15491 | | `jsonfile@6.1.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/jsonfile/6.1.0) | | `jsonschema@1.4.1` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/jsonschema/1.4.1) | +| `lodash@4.17.21` | CC0-1.0 | #2096 | | `math-intrinsics@1.1.0` | MIT | #17964 | | `mime-db@1.52.0` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/mime-db/1.52.0) | | `mime-types@2.1.35` | MIT | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/mime-types/2.1.35) | diff --git a/code/extensions/che-remote/package-lock.json b/code/extensions/che-remote/package-lock.json index da68f18d242..32679614b25 100644 --- a/code/extensions/che-remote/package-lock.json +++ b/code/extensions/che-remote/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.1", "license": "EPL-2.0", "dependencies": { - "@eclipse-che/che-devworkspace-generator": "7.99.0-next-aef9b62", - "axios": "^1.7.4", + "@eclipse-che/che-devworkspace-generator": "7.105.0-next-a6fff71", + "axios": "^1.8.3", "https": "^1.0.0", "js-yaml": "^4.0.0", "vscode-nls": "^5.0.0" @@ -592,9 +592,10 @@ "dev": true }, "node_modules/@devfile/api": { - "version": "2.3.0-1738342178", - "resolved": "https://registry.npmjs.org/@devfile/api/-/api-2.3.0-1738342178.tgz", - "integrity": "sha512-asgIDvCeBoSjk9cufBEZaJac5VHTaehaVBjxAyeehR4NZir2HOtgLMf26BYWLs2/PObmuen06PJgjr70kO3oGA==", + "version": "2.3.0-1747843475", + "resolved": "https://registry.npmjs.org/@devfile/api/-/api-2.3.0-1747843475.tgz", + "integrity": "sha512-w+H4g7D3FsSB1W672Qf8kcQX8FCg7Iztd7rmpZyx1xun3I02VE/Hn3tk0xqP89Ys8tMrJU3F3IwcSkP7bEWRKA==", + "license": "Apache-2.0", "dependencies": { "@types/node": "*", "@types/node-fetch": "^2.5.7", @@ -605,21 +606,25 @@ } }, "node_modules/@eclipse-che/che-devworkspace-generator": { - "version": "7.99.0-next-aef9b62", - "resolved": "https://registry.npmjs.org/@eclipse-che/che-devworkspace-generator/-/che-devworkspace-generator-7.99.0-next-aef9b62.tgz", - "integrity": "sha512-pE8Dls9rpfseROQ28cziHNkurV5+KOj3eKmFimdNuyjLbubLc/rU9MnOQ0iCm+u+Zr3SMpmEwrr+vYUYViGhGw==", + "version": "7.105.0-next-a6fff71", + "resolved": "https://registry.npmjs.org/@eclipse-che/che-devworkspace-generator/-/che-devworkspace-generator-7.105.0-next-a6fff71.tgz", + "integrity": "sha512-LzJNDZ167O0SxHb2fzqL/bwR5hXlfkTMm8rymk009QPOBUJQfdRs8RFvrZaQD0XPJpELZA+aUu65jx1cbGpU3A==", + "license": "EPL-2.0", "dependencies": { - "@devfile/api": "2.3.0-1738342178", - "axios": "^1.7.4", + "@devfile/api": "2.3.0-1747843475", "fs-extra": "^11.2.0", - "inversify": "^6.0.2", + "inversify": "^7.1.0", "js-yaml": "^4.0.0", "jsonc-parser": "^3.0.0", "jsonschema": "^1.4.1", + "lodash": "^4.17.21", "reflect-metadata": "^0.2.2" }, "bin": { "che-devworkspace-generator": "lib/entrypoint.js" + }, + "peerDependencies": { + "axios": "^1.8.3" } }, "node_modules/@eslint-community/eslint-utils": { @@ -713,6 +718,62 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@inversifyjs/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", + "integrity": "sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==", + "license": "MIT" + }, + "node_modules/@inversifyjs/container": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@inversifyjs/container/-/container-1.10.3.tgz", + "integrity": "sha512-uuB6fNd3XeFBtsysJXqOt0G114M69EpQCfM2G2XnhNYUb/osnHNGeubl2sMHd6SO08HbtAD8yNQOkPpxKeQTEw==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2", + "@inversifyjs/core": "5.3.3", + "@inversifyjs/plugin": "0.2.0", + "@inversifyjs/reflect-metadata-utils": "1.2.0" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, + "node_modules/@inversifyjs/core": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-5.3.3.tgz", + "integrity": "sha512-sx7msHtmgu/7gNPTMN9TzeBiE7y9y3dCEbLgtY8JOLP7okRPrrjTl7w6pd5RY00s/AovdDoE7BscNZVjqWIR0A==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2", + "@inversifyjs/prototype-utils": "0.1.2", + "@inversifyjs/reflect-metadata-utils": "1.2.0" + } + }, + "node_modules/@inversifyjs/plugin": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/plugin/-/plugin-0.2.0.tgz", + "integrity": "sha512-R/JAdkTSD819pV1zi0HP54mWHyX+H2m8SxldXRgPQarS3ySV4KPyRdosWcfB8Se0JJZWZLHYiUNiS6JvMWSPjw==", + "license": "MIT" + }, + "node_modules/@inversifyjs/prototype-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@inversifyjs/prototype-utils/-/prototype-utils-0.1.2.tgz", + "integrity": "sha512-WZAEycwVd8zVCPCQ7GRzuQmjYF7X5zbjI9cGigDbBoTHJ8y5US9om00IAp0RYislO+fYkMzgcB2SnlIVIzyESA==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2" + } + }, + "node_modules/@inversifyjs/reflect-metadata-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.2.0.tgz", + "integrity": "sha512-EvXdBP+IjAZ3QKWFWGDzWLacyng9RMohtZ0c0EPUmvbaF4wgfwrE6n92ilt9aGwdbE5roGmDTmoRSmvavVOtIA==", + "license": "MIT", + "peerDependencies": { + "reflect-metadata": "0.2.2" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1282,19 +1343,22 @@ "version": "2.6.12", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1496,9 +1560,10 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -1710,6 +1775,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2030,6 +2096,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2076,6 +2143,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2084,6 +2152,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2092,6 +2161,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2103,6 +2173,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2116,7 +2187,8 @@ "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" }, "node_modules/escalade": { "version": "3.2.0", @@ -2482,6 +2554,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2553,16 +2626,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -2588,6 +2662,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -2660,6 +2735,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2691,6 +2767,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2702,6 +2779,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -2865,9 +2943,18 @@ "dev": true }, "node_modules/inversify": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", - "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.5.4.tgz", + "integrity": "sha512-4UqlM8ALDzxsqQpK9ExQJAgnJoYhGJ88sr5v0ukwjpwIsE/ZKl9PyRtm9sWmViaHA4n2SergxC5sL3eeW+EWLQ==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "1.5.2", + "@inversifyjs/container": "1.10.3", + "@inversifyjs/core": "5.3.3" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } }, "node_modules/is-arrayish": { "version": "0.2.1", @@ -3881,8 +3968,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -3951,6 +4037,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4030,6 +4117,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -4048,17 +4136,20 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -4583,7 +4674,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", diff --git a/code/extensions/che-remote/package.json b/code/extensions/che-remote/package.json index 87225b7fed1..3a28710782a 100644 --- a/code/extensions/che-remote/package.json +++ b/code/extensions/che-remote/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "vscode-nls": "^5.0.0", - "axios": "^1.7.4", - "@eclipse-che/che-devworkspace-generator": "7.99.0-next-aef9b62", + "axios": "^1.8.3", + "@eclipse-che/che-devworkspace-generator": "7.105.0-next-a6fff71", "https": "^1.0.0", "js-yaml": "^4.0.0" }, diff --git a/code/extensions/che-remote/src/extension.ts b/code/extensions/che-remote/src/extension.ts index b4dea167f11..dd7181dcd64 100644 --- a/code/extensions/che-remote/src/extension.ts +++ b/code/extensions/che-remote/src/extension.ts @@ -191,7 +191,7 @@ async function updateDevfile(cheApi: any): Promise { devfilePath, editorContent: EDITOR_CONTENT_STUB, projects: [] - }, axiosInstance); + }, axiosInstance as unknown as any); } catch (error) { const action = await vscode.window.showErrorMessage('Failed to generate new Devfile Context.', { modal: true, diff --git a/code/extensions/configuration-editing/src/configurationEditingMain.ts b/code/extensions/configuration-editing/src/configurationEditingMain.ts index f791557a705..2578270c4a0 100644 --- a/code/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/code/extensions/configuration-editing/src/configurationEditingMain.ts @@ -62,6 +62,7 @@ function registerVariableCompletions(pattern: string): vscode.Disposable { { label: 'lineNumber', detail: vscode.l10n.t("The current selected line number in the active file") }, { label: 'selectedText', detail: vscode.l10n.t("The current selected text in the active file") }, { label: 'fileDirname', detail: vscode.l10n.t("The current opened file's dirname") }, + { label: 'fileDirnameBasename', detail: vscode.l10n.t("The current opened file's folder name") }, { label: 'fileExtname', detail: vscode.l10n.t("The current opened file's extension") }, { label: 'fileBasename', detail: vscode.l10n.t("The current opened file's basename") }, { label: 'fileBasenameNoExtension', detail: vscode.l10n.t("The current opened file's basename with no file extension") }, diff --git a/code/extensions/css-language-features/client/src/cssClient.ts b/code/extensions/css-language-features/client/src/cssClient.ts index f6e8fe3513e..4e90b3482e4 100644 --- a/code/extensions/css-language-features/client/src/cssClient.ts +++ b/code/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/code/extensions/css-language-features/client/src/node/cssClientMain.ts b/code/extensions/css-language-features/client/src/node/cssClientMain.ts index 96926979b2a..f634188bedf 100644 --- a/code/extensions/css-language-features/client/src/node/cssClientMain.ts +++ b/code/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/code/extensions/css-language-features/package-lock.json b/code/extensions/css-language-features/package-lock.json index c843143c01f..7ffe55ba0bc 100644 --- a/code/extensions/css-language-features/package-lock.json +++ b/code/extensions/css-language-features/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "vscode-languageclient": "^10.0.0-next.14", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/node": "20.x" @@ -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", @@ -116,9 +118,10 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" } } } diff --git a/code/extensions/css-language-features/package.json b/code/extensions/css-language-features/package.json index a24a5352dd6..a05f7af687e 100644 --- a/code/extensions/css-language-features/package.json +++ b/code/extensions/css-language-features/package.json @@ -995,7 +995,7 @@ }, "dependencies": { "vscode-languageclient": "^10.0.0-next.14", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/node": "20.x" diff --git a/code/extensions/css-language-features/server/package-lock.json b/code/extensions/css-language-features/server/package-lock.json index 9e560d6e77e..da345e38337 100644 --- a/code/extensions/css-language-features/server/package-lock.json +++ b/code/extensions/css-language-features/server/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.3", + "vscode-css-languageservice": "^6.3.5", "vscode-languageserver": "^10.0.0-next.11", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/mocha": "^9.1.1", @@ -49,15 +49,15 @@ "dev": true }, "node_modules/vscode-css-languageservice": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.3.tgz", - "integrity": "sha512-xXa+ftMPv6JxRgzkvPwZuDCafIdwDW3kyijGcfij1a2qBVScr2qli6MfgJzYm/AMYdbHq9I/4hdpKV0Thim2EA==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.5.tgz", + "integrity": "sha512-ehEIMXYPYEz/5Svi2raL9OKLpBt5dSAdoCFoLpo0TVFKrVpDemyuQwS3c3D552z/qQCg3pMp8oOLMObY6M3ajQ==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" } }, "node_modules/vscode-jsonrpc": { @@ -105,9 +105,10 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" } } } diff --git a/code/extensions/css-language-features/server/package.json b/code/extensions/css-language-features/server/package.json index 6898d432a82..c63252f6d8d 100644 --- a/code/extensions/css-language-features/server/package.json +++ b/code/extensions/css-language-features/server/package.json @@ -11,9 +11,9 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.3", + "vscode-css-languageservice": "^6.3.5", "vscode-languageserver": "^10.0.0-next.11", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/mocha": "^9.1.1", diff --git a/code/extensions/css-language-features/server/src/cssServer.ts b/code/extensions/css-language-features/server/src/cssServer.ts index c5db57340fd..8b365f41b6b 100644 --- a/code/extensions/css-language-features/server/src/cssServer.ts +++ b/code/extensions/css-language-features/server/src/cssServer.ts @@ -7,7 +7,7 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable, TextDocumentIdentifier, Range, FormattingOptions, TextEdit, Diagnostic } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position } from 'vscode-css-languageservice'; +import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position, CodeActionKind } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { runSafeAsync } from './utils/runner'; import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; @@ -119,7 +119,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) documentLinkProvider: { resolveProvider: false }, - codeActionProvider: true, + codeActionProvider: { + codeActionKinds: [CodeActionKind.QuickFix] + }, renameProvider: true, colorProvider: {}, foldingRangeProvider: true, @@ -286,7 +288,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) if (document) { await dataProvidersReady; const stylesheet = stylesheets.get(document); - return getLanguageService(document).doCodeActions(document, codeActionParams.range, codeActionParams.context, stylesheet); + return getLanguageService(document).doCodeActions2(document, codeActionParams.range, codeActionParams.context, stylesheet); } return []; }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token); diff --git a/code/extensions/debug-auto-launch/package-lock.json b/code/extensions/debug-auto-launch/package-lock.json index 84a1daab83b..d6a69a857f5 100644 --- a/code/extensions/debug-auto-launch/package-lock.json +++ b/code/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/code/extensions/emmet/package-lock.json b/code/extensions/emmet/package-lock.json index 745a18c4bc2..2de9cc7224e 100644 --- a/code/extensions/emmet/package-lock.json +++ b/code/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/code/extensions/git/package-lock.json b/code/extensions/git/package-lock.json index 2629a940444..16b71e40c37 100644 --- a/code/extensions/git/package-lock.json +++ b/code/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/code/extensions/git/package.json b/code/extensions/git/package.json index 6761f02ce5b..21e90d59ee4 100644 --- a/code/extensions/git/package.json +++ b/code/extensions/git/package.json @@ -34,7 +34,6 @@ "statusBarItemTooltip", "tabInputMultiDiff", "tabInputTextMerge", - "textDocumentEncoding", "textEditorDiffInformation", "timeline" ], @@ -252,6 +251,13 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.unstageChange", + "title": "%command.unstageChange%", + "category": "Git", + "icon": "$(remove)", + "enablement": "!operationInProgress" + }, { "command": "git.unstageFile", "title": "%command.unstage%", @@ -1078,6 +1084,10 @@ "command": "git.unstageSelectedRanges", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == git" }, + { + "command": "git.unstageChange", + "when": "false" + }, { "command": "git.clean", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -2220,11 +2230,15 @@ "scm/change/title": [ { "command": "git.stageChange", - "when": "config.git.enabled && !git.missing && originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResource =~ /^git\\:.*%22ref%22%3A%22%22%7D$/" }, { "command": "git.revertChange", - "when": "config.git.enabled && !git.missing && originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResource =~ /^git\\:.*%22ref%22%3A%22%22%7D$/" + }, + { + "command": "git.unstageChange", + "when": "false" } ], "timeline/item/context": [ @@ -3298,13 +3312,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 +3333,7 @@ "default": { "*": "error" }, - "markdownDescription": "%config.diagnosticsCommitHook.Sources%", + "markdownDescription": "%config.diagnosticsCommitHook.sources%", "scope": "resource" }, "git.discardUntrackedChangesToTrash": { diff --git a/code/extensions/git/package.nls.json b/code/extensions/git/package.nls.json index 52ba3819817..403f704e2f6 100644 --- a/code/extensions/git/package.nls.json +++ b/code/extensions/git/package.nls.json @@ -28,6 +28,7 @@ "command.revertChange": "Revert Change", "command.unstage": "Unstage Changes", "command.unstageAll": "Unstage All Changes", + "command.unstageChange": "Unstage Change", "command.unstageSelectedRanges": "Unstage Selected Ranges", "command.rename": "Rename", "command.clean": "Discard Changes", @@ -286,8 +287,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/code/extensions/git/src/blame.ts b/code/extensions/git/src/blame.ts index e4dda68c285..eb65a8ea0ab 100644 --- a/code/extensions/git/src/blame.ts +++ b/code/extensions/git/src/blame.ts @@ -5,7 +5,7 @@ import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, languages, HoverProvider, CancellationToken, Hover, TextDocument } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, getCommitShortHash, IDisposable } from './util'; +import { dispose, fromNow, getCommitShortHash, IDisposable, truncate } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation, Commit } from './git'; @@ -186,14 +186,10 @@ export class GitBlameController { } formatBlameInformationMessage(documentUri: Uri, template: string, blameInformation: BlameInformation): string { - const subject = blameInformation.subject && blameInformation.subject.length > this._subjectMaxLength - ? `${blameInformation.subject.substring(0, this._subjectMaxLength)}\u2026` - : blameInformation.subject; - const templateTokens = { hash: blameInformation.hash, hashShort: getCommitShortHash(documentUri, blameInformation.hash), - subject: emojify(subject ?? ''), + subject: emojify(truncate(blameInformation.subject ?? '', this._subjectMaxLength)), authorName: blameInformation.authorName ?? '', authorEmail: blameInformation.authorEmail ?? '', authorDate: new Date(blameInformation.authorDate ?? new Date()).toLocaleString(), @@ -257,6 +253,7 @@ export class GitBlameController { const authorDate = commitInformation?.authorDate ?? blameInformation.authorDate; const avatar = commitAvatar ? `![${authorName}](${commitAvatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` : '$(account)'; + if (authorName) { if (authorEmail) { const emailTitle = l10n.t('Email'); @@ -276,7 +273,8 @@ export class GitBlameController { } // Subject | Message - markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`); + const message = commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? ''; + markdownString.appendMarkdown(`${emojify(message.replace(/\r\n|\r|\n/g, '\n\n'))}\n\n`); markdownString.appendMarkdown(`---\n\n`); // Short stats @@ -577,6 +575,7 @@ export class GitBlameController { } class GitBlameEditorDecoration implements HoverProvider { + private _template = ''; private _decoration: TextEditorDecorationType; private _hoverDisposable: IDisposable | undefined; @@ -636,6 +635,10 @@ class GitBlameEditorDecoration implements HoverProvider { return; } + // Cache the decoration template + const config = workspace.getConfiguration('git'); + this._template = config.get('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})'); + this._registerHoverProvider(); this._onDidChangeBlameInformation(); } @@ -666,12 +669,9 @@ class GitBlameEditorDecoration implements HoverProvider { } // Set decorations for the editor - const config = workspace.getConfiguration('git'); - const template = config.get('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})'); - const decorations = blameInformation.map(blame => { const contentText = typeof blame.blameInformation !== 'string' - ? this._controller.formatBlameInformationMessage(textEditor.document.uri, template, blame.blameInformation) + ? this._controller.formatBlameInformationMessage(textEditor.document.uri, this._template, blame.blameInformation) : blame.blameInformation; return this._createDecoration(blame.lineNumber, contentText); @@ -711,6 +711,7 @@ class GitBlameEditorDecoration implements HoverProvider { } class GitBlameStatusBarItem { + private _template = ''; private _statusBarItem: StatusBarItem; private _disposables: IDisposable[] = []; @@ -721,14 +722,21 @@ class GitBlameStatusBarItem { workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables); + + this._onDidChangeConfiguration(); } - private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.commitShortHashLength') && + private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { + if (e && + !e.affectsConfiguration('git.commitShortHashLength') && !e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } + // Cache the decoration template + const config = workspace.getConfiguration('git'); + this._template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); + this._onDidChangeBlameInformation(); } @@ -749,11 +757,8 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = l10n.t('Git Blame Information'); this._statusBarItem.command = undefined; } else { - const config = workspace.getConfiguration('git'); - const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage( - window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; + window.activeTextEditor.document.uri, this._template, blameInformation[0].blameInformation)}`; this._statusBarItem.tooltip2 = (cancellationToken: CancellationToken) => { return this._provideTooltip(window.activeTextEditor!.document.uri, diff --git a/code/extensions/git/src/commands.ts b/code/extensions/git/src/commands.ts index 176a95b5432..fc38ccad68a 100644 --- a/code/extensions/git/src/commands.ts +++ b/code/extensions/git/src/commands.ts @@ -12,9 +12,9 @@ import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSou import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; -import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; +import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { DiagnosticSeverityConfig, dispose, fromNow, getCommitShortHash, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; +import { DiagnosticSeverityConfig, dispose, fromNow, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -632,59 +632,59 @@ 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; } - const changes: Uri[] = []; + const resources: Uri[] = []; if (repository.indexGroup.resourceStates.length > 0) { // Staged files - changes.push(...repository.indexGroup.resourceStates.map(r => r.resourceUri)); + resources.push(...repository.indexGroup.resourceStates.map(r => r.resourceUri)); } else if (options.all === 'tracked') { // Tracked files - changes.push(...repository.workingTreeGroup.resourceStates + resources.push(...repository.workingTreeGroup.resourceStates .filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED) .map(r => r.resourceUri)); } else { // All files - changes.push(...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri)); - changes.push(...repository.untrackedGroup.resourceStates.map(r => r.resourceUri)); + resources.push(...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri)); + resources.push(...repository.untrackedGroup.resourceStates.map(r => r.resourceUri)); } - const diagnostics = languages.getDiagnostics(); - const changesDiagnostics = diagnostics.filter(([uri, diags]) => { - // File - if (uri.scheme !== 'file' || !changes.find(c => pathEquals(c.fsPath, uri.fsPath))) { - return false; - } + const diagnostics: Map = new Map(); - // Diagnostics - return diags.find(d => { - // No source or ignored source - if (!d.source || (Object.keys(sourceSeverity).includes(d.source) && sourceSeverity[d.source] === 'none')) { - return false; - } + for (const resource of resources) { + const unresolvedDiagnostics = languages.getDiagnostics(resource) + .filter(d => { + // No source or ignored source + if (!d.source || (Object.keys(sourceSeverity).includes(d.source) && sourceSeverity[d.source] === 'none')) { + return false; + } - // Source severity - if (Object.keys(sourceSeverity).includes(d.source) && - d.severity <= toDiagnosticSeverity(sourceSeverity[d.source])) { - return true; - } + // Source severity + if (Object.keys(sourceSeverity).includes(d.source) && + d.severity <= toDiagnosticSeverity(sourceSeverity[d.source])) { + return true; + } - // Wildcard severity - if (Object.keys(sourceSeverity).includes('*') && - d.severity <= toDiagnosticSeverity(sourceSeverity['*'])) { - return true; - } + // Wildcard severity + if (Object.keys(sourceSeverity).includes('*') && + d.severity <= toDiagnosticSeverity(sourceSeverity['*'])) { + return true; + } - return false; - }); - }); + return false; + }); + + if (unresolvedDiagnostics.length > 0) { + diagnostics.set(resource, unresolvedDiagnostics.length); + } + } - if (changesDiagnostics.length === 0) { + if (diagnostics.size === 0) { return true; } @@ -692,9 +692,9 @@ async function evaluateDiagnosticsCommitHook(repository: Repository, options: Co const commit = l10n.t('Commit Anyway'); const view = l10n.t('View Problems'); - const message = changesDiagnostics.length === 1 - ? l10n.t('The following file has unresolved diagnostics: \'{0}\'.\n\nHow would you like to proceed?', path.basename(changesDiagnostics[0][0].fsPath)) - : l10n.t('There are {0} files that have unresolved diagnostics.\n\nHow would you like to proceed?', changesDiagnostics.length); + const message = diagnostics.size === 1 + ? l10n.t('The following file has unresolved diagnostics: \'{0}\'.\n\nHow would you like to proceed?', path.basename(diagnostics.keys().next().value!.fsPath)) + : l10n.t('There are {0} files that have unresolved diagnostics.\n\nHow would you like to proceed?', diagnostics.size); const choice = await window.showWarningMessage(message, { modal: true }, commit, view); @@ -1020,18 +1020,37 @@ export class CommandCenter { } } - @command('git.continueInLocalClone') - async continueInLocalClone(): Promise { - if (this.model.repositories.length === 0) { return; } - - // Pick a single repository to continue working on in a local clone if there's more than one - const items = this.model.repositories.reduce<(QuickPickItem & { repository: Repository })[]>((items, repository) => { + private getRepositoriesWithRemote(repositories: Repository[]) { + return repositories.reduce<(QuickPickItem & { repository: Repository })[]>((items, repository) => { const remote = repository.remotes.find((r) => r.name === repository.HEAD?.upstream?.remote); if (remote?.pushUrl) { items.push({ repository: repository, label: remote.pushUrl }); } return items; }, []); + } + + @command('git.continueInLocalClone') + async continueInLocalClone(): Promise { + if (this.model.repositories.length === 0) { return; } + + // Pick a single repository to continue working on in a local clone if there's more than one + let items = this.getRepositoriesWithRemote(this.model.repositories); + + // We have a repository but there is no remote URL (e.g. git init) + if (items.length === 0) { + const pick = this.model.repositories.length === 1 + ? { repository: this.model.repositories[0] } + : await window.showQuickPick(this.model.repositories.map((i) => ({ repository: i, label: i.root })), { canPickMany: false, placeHolder: l10n.t('Choose which repository to publish') }); + if (!pick) { return; } + + await this.publish(pick.repository); + + items = this.getRepositoriesWithRemote([pick.repository]); + if (items.length === 0) { + return; + } + } let selection = items[0]; if (items.length > 1) { @@ -1955,16 +1974,6 @@ export class CommandCenter { const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; - if (!isGitUri(modifiedUri)) { - return; - } - - const { ref } = fromGitUri(modifiedUri); - - if (ref !== '') { - return; - } - const repository = this.model.getRepository(modifiedUri); if (!repository) { return; @@ -1989,6 +1998,7 @@ export class CommandCenter { const originalUri = toGitUri(resource.original, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); + const selectedDiffs = indexLineChanges .map(change => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null)) .filter(c => !!c) as LineChange[]; @@ -1998,13 +2008,20 @@ export class CommandCenter { return; } - const invertedDiffs = selectedDiffs.map(invertLineChange); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffs: ${JSON.stringify(selectedDiffs)}`); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`); - const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); - await repository.stage(modifiedUri, result, modifiedDocument.encoding); + if (modifiedUri.scheme === 'file') { + // Editor + this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(selectedDiffs)}`); + await this._unstageChanges(textEditor, selectedDiffs); + return; + } + + const selectedDiffsInverted = selectedDiffs.map(invertLineChange); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffsInverted: ${JSON.stringify(selectedDiffsInverted)}`); + + const result = applyLineChanges(modifiedDocument, originalDocument, selectedDiffsInverted); + await repository.stage(modifiedDocument.uri, result, modifiedDocument.encoding); } @command('git.unstageFile') @@ -2029,6 +2046,49 @@ export class CommandCenter { await repository.revert(resources); } + @command('git.unstageChange') + async unstageChange(uri: Uri, changes: LineChange[], index: number): Promise { + if (!uri) { + return; + } + + const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0]; + if (!textEditor) { + return; + } + + await this._unstageChanges(textEditor, [changes[index]]); + } + + private async _unstageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { + const modifiedDocument = textEditor.document; + const modifiedUri = modifiedDocument.uri; + + if (modifiedUri.scheme !== 'file') { + return; + } + + const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor); + if (!workingTreeDiffInformation) { + return; + } + + // Approach to unstage change(s): + // - use file on disk as original document + // - revert all changes from the working tree + // - revert the specify change(s) from the index + const workingTreeDiffs = toLineChanges(workingTreeDiffInformation); + const workingTreeDiffsInverted = workingTreeDiffs.map(invertLineChange); + const changesInverted = changes.map(invertLineChange); + const diffsInverted = [...changesInverted, ...workingTreeDiffsInverted].sort(compareLineChanges); + + const originalUri = toGitUri(modifiedUri, 'HEAD'); + const originalDocument = await workspace.openTextDocument(originalUri); + const result = applyLineChanges(modifiedDocument, originalDocument, diffsInverted); + + await this.runByRepository(modifiedUri, async (repository, resource) => + await repository.stage(resource, result, modifiedDocument.encoding)); + } @command('git.clean') async clean(...resourceStates: SourceControlResourceState[]): Promise { @@ -2867,6 +2927,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 +2945,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 @@ -4536,8 +4604,11 @@ export class CommandCenter { } const rootUri = Uri.file(repository.root); + const config = workspace.getConfiguration('git', rootUri); + const commitShortHashLength = config.get('commitShortHashLength', 7); + const commit = await repository.getCommit(historyItemId); - const title = `${getCommitShortHash(rootUri, historyItemId)} - ${truncate(commit.message)}`; + const title = `${truncate(historyItemId, commitShortHashLength, false)} - ${truncate(commit.message)}`; const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree(); const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` }); diff --git a/code/extensions/git/src/editSessionIdentityProvider.ts b/code/extensions/git/src/editSessionIdentityProvider.ts index 6a0a31774a1..8380f03ecfd 100644 --- a/code/extensions/git/src/editSessionIdentityProvider.ts +++ b/code/extensions/git/src/editSessionIdentityProvider.ts @@ -13,11 +13,18 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit private providerRegistration: vscode.Disposable; constructor(private model: Model) { - this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this); - - vscode.workspace.onWillCreateEditSessionIdentity((e) => { - e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder)); - }); + this.providerRegistration = vscode.Disposable.from( + vscode.workspace.registerEditSessionIdentityProvider('file', this), + vscode.workspace.onWillCreateEditSessionIdentity((e) => { + e.waitUntil( + this._onWillCreateEditSessionIdentity(e.workspaceFolder).catch(err => { + if (err instanceof vscode.CancellationError) { + throw err; + } + }) + ); + }) + ); } dispose() { @@ -81,9 +88,23 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit await repository.status(); - // If this branch hasn't been published to the remote yet, - // ensure that it is published before Continue On is invoked - if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) { + if (!repository.HEAD?.commit) { + // Handle publishing empty repository with no commits + + const yes = vscode.l10n.t('Yes'); + const selection = await vscode.window.showInformationMessage( + vscode.l10n.t('Would you like to publish this repository to continue working on it elsewhere?'), + { modal: true }, + yes + ); + if (selection !== yes) { + throw new vscode.CancellationError(); + } + await repository.commit('Initial commit', { all: true }); + await vscode.commands.executeCommand('git.publish'); + } else if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) { + // If this branch hasn't been published to the remote yet, + // ensure that it is published before Continue On is invoked const publishBranch = vscode.l10n.t('Publish Branch'); const selection = await vscode.window.showInformationMessage( diff --git a/code/extensions/git/src/git.ts b/code/extensions/git/src/git.ts index e42ca1e00d8..f6b487e8b93 100644 --- a/code/extensions/git/src/git.ts +++ b/code/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 relative() + filePath = sanitizeRelativePath(path.relative(this.repositoryRoot, filePath)); + this.logger.trace(`[Git][sanitizeRelativePath] relativePath (fallback): ${filePath}`); + return filePath; + } } diff --git a/code/extensions/git/src/historyProvider.ts b/code/extensions/git/src/historyProvider.ts index 8e5a8f9d2f1..886510b0031 100644 --- a/code/extensions/git/src/historyProvider.ts +++ b/code/extensions/git/src/historyProvider.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, truncate } from './util'; import { toMultiFileDiffEditorUris } from './uri'; import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; @@ -14,40 +14,6 @@ import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; -function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { - const rootUri = Uri.file(repository.root); - - switch (ref.type) { - case RefType.RemoteHead: - return { - id: `refs/remotes/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Remote branch at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('cloud'), - category: l10n.t('remote branches') - }; - case RefType.Tag: - return { - id: `refs/tags/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Tag at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('tag'), - category: l10n.t('tags') - }; - default: - return { - id: `refs/heads/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? getCommitShortHash(rootUri, ref.commit) : undefined, - revision: ref.commit, - icon: new ThemeIcon('git-branch'), - category: l10n.t('branches') - }; - } -} - function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number { const getOrder = (ref: SourceControlHistoryItemRef): number => { if (ref.id.startsWith('refs/heads/')) { @@ -93,6 +59,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private _HEAD: Branch | undefined; private _historyItemRefs: SourceControlHistoryItemRef[] = []; + private commitShortHashLength = 7; private historyItemDecorations = new Map(); private disposables: Disposable[] = []; @@ -102,12 +69,24 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private readonly repository: Repository, private readonly logger: LogOutputChannel ) { + this.disposables.push(workspace.onDidChangeConfiguration(this.onDidChangeConfiguration)); + this.onDidChangeConfiguration(); + const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly); this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this)); this.disposables.push(window.registerFileDecorationProvider(this)); } + private onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { + if (e && !e.affectsConfiguration('git.commitShortHashLength')) { + return; + } + + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + this.commitShortHashLength = config.get('commitShortHashLength', 7); + } + private async onDidRunWriteOperation(result: OperationResult): Promise { if (!this.repository.HEAD) { this.logger.trace('[GitHistoryProvider][onDidRunWriteOperation] repository.HEAD is undefined'); @@ -119,7 +98,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Refs (alphabetically) const historyItemRefs = this.repository.refs - .map(ref => toSourceControlHistoryItemRef(this.repository, ref)) + .map(ref => this.toSourceControlHistoryItemRef(ref)) .sort((a, b) => a.id.localeCompare(b.id)); const delta = deltaHistoryItemRefs(this._historyItemRefs, historyItemRefs); @@ -241,13 +220,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec for (const ref of refs) { switch (ref.type) { case RefType.RemoteHead: - remoteBranches.push(toSourceControlHistoryItemRef(this.repository, ref)); + remoteBranches.push(this.toSourceControlHistoryItemRef(ref)); break; case RefType.Tag: - tags.push(toSourceControlHistoryItemRef(this.repository, ref)); + tags.push(this.toSourceControlHistoryItemRef(ref)); break; default: - branches.push(toSourceControlHistoryItemRef(this.repository, ref)); + branches.push(this.toSourceControlHistoryItemRef(ref)); break; } } @@ -305,7 +284,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const newLineIndex = message.indexOf('\n'); const subject = newLineIndex !== -1 - ? `${message.substring(0, newLineIndex)}\u2026` + ? `${truncate(message, newLineIndex)}` : message; const avatarUrl = commitAvatars?.get(commit.hash); @@ -319,7 +298,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec author: commit.authorName, authorEmail: commit.authorEmail, authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'), - displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), + displayId: truncate(commit.hash, this.commitShortHashLength, false), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, references: references.length !== 0 ? references : undefined @@ -469,6 +448,38 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } } + private toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { + switch (ref.type) { + case RefType.RemoteHead: + return { + id: `refs/remotes/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Remote branch at {0}', truncate(ref.commit, this.commitShortHashLength, false)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('cloud'), + category: l10n.t('remote branches') + }; + case RefType.Tag: + return { + id: `refs/tags/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Tag at {0}', truncate(ref.commit, this.commitShortHashLength, false)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('tag'), + category: l10n.t('tags') + }; + default: + return { + id: `refs/heads/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? truncate(ref.commit, this.commitShortHashLength, false) : undefined, + revision: ref.commit, + icon: new ThemeIcon('git-branch'), + category: l10n.t('branches') + }; + } + } + dispose(): void { dispose(this.disposables); } diff --git a/code/extensions/git/src/main.ts b/code/extensions/git/src/main.ts index 8205d8de69f..a55c103c9ef 100644 --- a/code/extensions/git/src/main.ts +++ b/code/extensions/git/src/main.ts @@ -27,7 +27,6 @@ import { GitPostCommitCommandsProvider } from './postCommitCommands'; import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider'; import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics'; import { GitBlameController } from './blame'; -import { StagedResourceQuickDiffProvider } from './repository'; const deactivateTasks: { (): Promise }[] = []; @@ -117,7 +116,6 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, new GitBlameController(model), new GitTimelineProvider(model, cc), new GitEditSessionIdentityProvider(model), - new StagedResourceQuickDiffProvider(model), new TerminalShellExecutionManager(model, logger) ); diff --git a/code/extensions/git/src/model.ts b/code/extensions/git/src/model.ts index d17d118de1c..74486e6a090 100644 --- a/code/extensions/git/src/model.ts +++ b/code/extensions/git/src/model.ts @@ -640,7 +640,8 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); - this.logger.info(`[Model][openRepository] Opened repository: ${repository.root}`); + this.logger.info(`[Model][openRepository] Opened repository (path): ${repository.root}`); + this.logger.info(`[Model][openRepository] Opened repository (real path): ${repository.rootRealPath ?? repository.root}`); // Do not await this, we want SCM // to know about the repo asap diff --git a/code/extensions/git/src/repository.ts b/code/extensions/git/src/repository.ts index 910256ff10a..f2c8146a407 100644 --- a/code/extensions/git/src/repository.ts +++ b/code/extensions/git/src/repository.ts @@ -68,15 +68,13 @@ export class Resource implements SourceControlResourceState { return 'U'; case Status.IGNORED: return 'I'; - case Status.DELETED_BY_THEM: - return 'D'; - case Status.DELETED_BY_US: - return 'D'; case Status.INDEX_COPIED: return 'C'; case Status.BOTH_DELETED: case Status.ADDED_BY_US: + case Status.DELETED_BY_THEM: case Status.ADDED_BY_THEM: + case Status.DELETED_BY_US: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows @@ -892,6 +890,7 @@ export class Repository implements Disposable { this._sourceControl = scm.createSourceControl('git', 'Git', root); this._sourceControl.quickDiffProvider = this; + this._sourceControl.secondaryQuickDiffProvider = new StagedResourceQuickDiffProvider(this, logger); this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); this._sourceControl.historyProvider = this._historyProvider; @@ -1022,10 +1021,12 @@ export class Repository implements Disposable { * Quick diff label */ get label(): string { - return l10n.t('Git local changes (working tree)'); + return l10n.t('Git Local Changes (Working Tree)'); } provideOriginalResource(uri: Uri): Uri | undefined { + this.logger.trace(`[Repository][provideOriginalResource] Resource: ${uri.toString()}`); + if (uri.scheme !== 'file') { return; } @@ -1063,7 +1064,10 @@ export class Repository implements Disposable { return undefined; } - return toGitUri(uri, '', { replaceFileExtension: true }); + const originalResource = toGitUri(uri, '', { replaceFileExtension: true }); + this.logger.trace(`[Repository][provideOriginalResource] Original resource: ${originalResource.toString()}`); + + return originalResource; } async getInputTemplate(): Promise { @@ -1222,7 +1226,7 @@ export class Repository implements Disposable { async stage(resource: Uri, contents: string, encoding: string): Promise { await this.run(Operation.Stage, async () => { - const data = await workspace.encode(contents, resource, { encoding }); + const data = await workspace.encode(contents, { encoding }); await this.repository.stage(resource.fsPath, data); this._onDidChangeOriginalResource.fire(resource); @@ -1235,6 +1239,9 @@ export class Repository implements Disposable { Operation.RevertFiles(!this.optimisticUpdateEnabled()), async () => { await this.repository.revert('HEAD', resources.map(r => r.fsPath)); + for (const resource of resources) { + this._onDidChangeOriginalResource.fire(resource); + } this.closeDiffEditors([...resources.length !== 0 ? resources.map(r => r.fsPath) : this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)], []); @@ -1974,12 +1981,12 @@ export class Repository implements Disposable { return await this.run(Operation.Show, async () => { try { const content = await this.repository.buffer(ref, filePath); - return await workspace.decode(content, Uri.file(filePath)); + return await workspace.decode(content, { uri: Uri.file(filePath) }); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { const gitFilePath = await this.repository.getGitFilePath(ref, filePath); const content = await this.repository.buffer(ref, gitFilePath); - return await workspace.decode(content, Uri.file(filePath)); + return await workspace.decode(content, { uri: Uri.file(filePath) }); } throw err; @@ -2798,30 +2805,24 @@ export class Repository implements Disposable { } export class StagedResourceQuickDiffProvider implements QuickDiffProvider { - readonly visible: boolean = false; + readonly label = l10n.t('Git Local Changes (Index)'); - private _disposables: IDisposable[] = []; - - constructor(private readonly _repositoryResolver: IRepositoryResolver) { - this._disposables.push(window.registerQuickDiffProvider({ scheme: 'file' }, this, l10n.t('Git local changes (working tree + index)'))); - } + constructor( + private readonly _repository: Repository, + private readonly logger: LogOutputChannel + ) { } provideOriginalResource(uri: Uri): Uri | undefined { - // Ignore resources outside a repository - const repository = this._repositoryResolver.getRepository(uri); - if (!repository) { - return undefined; - } + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource: ${uri.toString()}`); // Ignore resources that are not in the index group - if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); return undefined; } - return toGitUri(uri, 'HEAD', { replaceFileExtension: true }); - } - - dispose() { - this._disposables = dispose(this._disposables); + const originalResource = toGitUri(uri, 'HEAD', { replaceFileExtension: true }); + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Original resource: ${originalResource.toString()}`); + return originalResource; } } diff --git a/code/extensions/git/src/staging.ts b/code/extensions/git/src/staging.ts index ec7232bec44..32df8240916 100644 --- a/code/extensions/git/src/staging.ts +++ b/code/extensions/git/src/staging.ts @@ -185,11 +185,32 @@ export function toLineChanges(diffInformation: TextEditorDiffInformation): LineC }); } +export function compareLineChanges(a: LineChange, b: LineChange): number { + let result = a.modifiedStartLineNumber - b.modifiedStartLineNumber; + + if (result !== 0) { + return result; + } + + result = a.modifiedEndLineNumber - b.modifiedEndLineNumber; + + if (result !== 0) { + return result; + } + + result = a.originalStartLineNumber - b.originalStartLineNumber; + + if (result !== 0) { + return result; + } + + return a.originalEndLineNumber - b.originalEndLineNumber; +} + export function getIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { - // Diff Editor (Index) + // Diff Editor (Index) | Text Editor return textEditor.diffInformation?.find(diff => - diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD' && - diff.modified && isGitUri(diff.modified) && fromGitUri(diff.modified).ref === ''); + diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD'); } export function getWorkingTreeDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { diff --git a/code/extensions/git/src/test/smoke.test.ts b/code/extensions/git/src/test/smoke.test.ts index b39cbd38563..4e3b71c7153 100644 --- a/code/extensions/git/src/test/smoke.test.ts +++ b/code/extensions/git/src/test/smoke.test.ts @@ -13,7 +13,7 @@ import { GitExtension, API, Repository, Status } from '../api/git'; import { eventToPromise } from '../util'; suite('git smoke test', function () { - const cwd = fs.realpathSync(workspace.workspaceFolders![0].uri.fsPath); + const cwd = workspace.workspaceFolders![0].uri.fsPath; function file(relativePath: string) { return path.join(cwd, relativePath); @@ -63,7 +63,7 @@ suite('git smoke test', function () { } assert.strictEqual(git.repositories.length, 1); - assert.strictEqual(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); + assert.strictEqual(git.repositories[0].rootUri.fsPath, cwd); repository = git.repositories[0]; }); diff --git a/code/extensions/git/src/timelineProvider.ts b/code/extensions/git/src/timelineProvider.ts index ae55d427bcc..76a3f92dde1 100644 --- a/code/extensions/git/src/timelineProvider.ts +++ b/code/extensions/git/src/timelineProvider.ts @@ -10,7 +10,7 @@ import { debounce } from './decorators'; import { emojify, ensureEmojis } from './emoji'; import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; -import { getCommitShortHash } from './util'; +import { truncate } from './util'; import { CommitShortStat } from './git'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { AvatarQuery, AvatarQueryCommit } from './api/git'; @@ -35,7 +35,7 @@ export class GitTimelineItem extends TimelineItem { contextValue: string ) { const index = message.indexOf('\n'); - const label = index !== -1 ? `${message.substring(0, index)} \u2026` : message; + const label = index !== -1 ? `${truncate(message, index)}` : message; super(label, timestamp); @@ -54,7 +54,7 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(uri: Uri, hash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { + setItemDetails(uri: Uri, hash: string | undefined, shortHash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { this.tooltip = new MarkdownString('', true); this.tooltip.isTrusted = true; @@ -91,10 +91,10 @@ export class GitTimelineItem extends TimelineItem { this.tooltip.appendMarkdown(`${labels.join(', ')}\n\n`); } - if (hash) { + if (hash && shortHash) { this.tooltip.appendMarkdown(`---\n\n`); - this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('Open Commit')}")`); + this.tooltip.appendMarkdown(`[\`$(git-commit) ${shortHash} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('Open Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); @@ -173,8 +173,6 @@ export class GitTimelineProvider implements TimelineProvider { ); } - const config = workspace.getConfiguration('git.timeline'); - // TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo? let limit: number | undefined; @@ -215,9 +213,11 @@ export class GitTimelineProvider implements TimelineProvider { const dateFormatter = new Intl.DateTimeFormat(env.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); - const dateType = config.get<'committed' | 'authored'>('date'); - const showAuthor = config.get('showAuthor'); - const showUncommitted = config.get('showUncommitted'); + const config = workspace.getConfiguration('git', Uri.file(repo.root)); + const dateType = config.get<'committed' | 'authored'>('timeline.date'); + const showAuthor = config.get('timeline.showAuthor'); + const showUncommitted = config.get('timeline.showUncommitted'); + const commitShortHashLength = config.get('commitShortHashLength') ?? 7; const openComparison = l10n.t('Open Comparison'); @@ -253,7 +253,7 @@ export class GitTimelineProvider implements TimelineProvider { const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : []; const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; - item.setItemDetails(uri, c.hash, avatars?.get(c.hash), c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); + item.setItemDetails(uri, c.hash, truncate(c.hash, commitShortHashLength, false), avatars?.get(c.hash), c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -278,7 +278,7 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(uri, undefined, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -300,7 +300,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); item.iconPath = new ThemeIcon('circle-outline'); item.description = ''; - item.setItemDetails(uri, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(uri, undefined, undefined, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { diff --git a/code/extensions/git/src/util.ts b/code/extensions/git/src/util.ts index 7dd1cbafbdf..28d8c27fbc5 100644 --- a/code/extensions/git/src/util.ts +++ b/code/extensions/git/src/util.ts @@ -291,8 +291,8 @@ export function detectUnicodeEncoding(buffer: Buffer): Encoding | null { return null; } -export function truncate(value: string, maxLength = 20): string { - return value.length <= maxLength ? value : `${value.substring(0, maxLength)}\u2026`; +export function truncate(value: string, maxLength = 20, ellipsis = true): string { + return value.length <= maxLength ? value : `${value.substring(0, maxLength)}${ellipsis ? '\u2026' : ''}`; } function normalizePath(path: string): string { @@ -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/code/extensions/git/tsconfig.json b/code/extensions/git/tsconfig.json index fc72bd70742..c79be520be9 100644 --- a/code/extensions/git/tsconfig.json +++ b/code/extensions/git/tsconfig.json @@ -24,7 +24,6 @@ "../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", - "../../src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", "../types/lib.textEncoder.d.ts" diff --git a/code/extensions/github/package-lock.json b/code/extensions/github/package-lock.json index cf7317a40a4..1b7dc727a92 100644 --- a/code/extensions/github/package-lock.json +++ b/code/extensions/github/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@octokit/graphql": "8.2.0", + "@octokit/graphql": "5.0.5", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "21.1.0", + "@octokit/rest": "19.0.4", "@vscode/extension-telemetry": "^0.9.8", "tunnel": "^0.0.6" }, @@ -147,57 +147,96 @@ "license": "MIT" }, "node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", - "license": "MIT", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.1.tgz", + "integrity": "sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==", + "dependencies": { + "@octokit/types": "^7.0.0" + }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/auth-token/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/auth-token/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/core": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz", - "integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==", - "license": "MIT", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.5.tgz", + "integrity": "sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==", "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.1.2", - "@octokit/request": "^9.2.1", - "@octokit/request-error": "^6.1.7", - "@octokit/types": "^13.6.2", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^7.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/endpoint": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", - "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.1.tgz", + "integrity": "sha512-/wTXAJwt0HzJ2IeE4kQXO+mBScfzyCkI0hMtkIaqyXd9zg76OpOfNQfHL9FlaxAV2RsNiOXZibVWloy8EexENg==", "dependencies": { - "@octokit/types": "^13.6.2", - "universal-user-agent": "^7.0.2" + "@octokit/types": "^7.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/graphql": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.0.tgz", - "integrity": "sha512-gejfDywEml/45SqbWTWrhfwvLBrcGYhOn50sPOjIeVvH6i7D16/9xcFA8dAJNp2HMcd+g4vru41g4E2RBiZvfQ==", - "license": "MIT", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", + "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", "dependencies": { - "@octokit/request": "^9.1.4", - "@octokit/types": "^13.8.0", - "universal-user-agent": "^7.0.0" + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/graphql-schema": { @@ -210,103 +249,148 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-17.1.0.tgz", + "integrity": "sha512-rnI26BAITDZTo5vqFOmA7oX4xRd18rO+gcK4MiTpJmsRMxAw0JmevNjPsjpry1bb9SVNo56P/0kbiyXXa4QluA==" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.4.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz", - "integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==", - "license": "MIT", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.0.tgz", + "integrity": "sha512-8otLCIK9esfmOCY14CBnG/xPqv0paf14rc+s9tHpbOpeFwrv5CnECKW1qdqMAT60ngAa9eB1bKQ+l2YCpi0HPQ==", "dependencies": { - "@octokit/types": "^13.7.0" + "@octokit/types": "^7.2.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/plugin-request-log": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz", - "integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==", - "license": "MIT", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.4.0.tgz", + "integrity": "sha512-YP4eUqZ6vORy/eZOTdil1ZSrMt0kv7i/CVw+HhC2C0yJN+IqTc/rot957JQ7JfyeJD6HZOjLg6Jp1o9cPhI9KA==", "dependencies": { - "@octokit/types": "^13.8.0" + "@octokit/types": "^7.2.0", + "deprecation": "^2.3.1" }, "engines": { - "node": ">= 18" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/request": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.1.tgz", - "integrity": "sha512-TqHLIdw1KFvx8WvLc7Jv94r3C3+AzKY2FWq7c20zvrxmCIa6MCVkLCE/826NCXnml3LFJjLsidDh1BhMaGEDQw==", - "license": "MIT", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", + "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", "dependencies": { - "@octokit/endpoint": "^10.1.3", - "@octokit/request-error": "^6.1.6", - "@octokit/types": "^13.6.2", - "fast-content-type-parse": "^2.0.0", - "universal-user-agent": "^7.0.2" + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^7.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/request-error": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", - "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", - "license": "MIT", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", + "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", "dependencies": { - "@octokit/types": "^13.6.2" + "@octokit/types": "^7.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/rest": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.0.tgz", - "integrity": "sha512-93iLxcKDJboUpmnUyeJ6cRIi7z7cqTZT1K7kRK4LobGxwTwpsa+2tQQbRQNGy7IFDEAmrtkf4F4wBj3D5rVlJQ==", - "license": "MIT", + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.4.tgz", + "integrity": "sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==", "dependencies": { - "@octokit/core": "^6.1.3", - "@octokit/plugin-paginate-rest": "^11.4.0", - "@octokit/plugin-request-log": "^5.3.1", - "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + "@octokit/core": "^4.0.0", + "@octokit/plugin-paginate-rest": "^4.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.2.0.tgz", + "integrity": "sha512-xySzJG4noWrIBFyMu4lg4tu9vAgNg9S0aoLRONhAEz6ueyi1evBzb40HitIosaYS4XOexphG305IVcLrIX/30g==", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^17.1.0" } }, "node_modules/@types/node": { @@ -333,26 +417,14 @@ } }, "node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "license": "Apache-2.0" - }, - "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, "node_modules/graphql": { "version": "16.8.1", @@ -376,6 +448,46 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -396,10 +508,28 @@ "dev": true }, "node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "license": "ISC" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" } } } diff --git a/code/extensions/github/package.json b/code/extensions/github/package.json index 86adc2ddc4e..524cee5bbea 100644 --- a/code/extensions/github/package.json +++ b/code/extensions/github/package.json @@ -227,9 +227,9 @@ "watch": "gulp watch-extension:github" }, "dependencies": { - "@octokit/graphql": "8.2.0", + "@octokit/graphql": "5.0.5", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "21.1.0", + "@octokit/rest": "19.0.4", "tunnel": "^0.0.6", "@vscode/extension-telemetry": "^0.9.8" }, diff --git a/code/extensions/go/cgmanifest.json b/code/extensions/go/cgmanifest.json index a6dbd5d1bf0..7d5ee20f828 100644 --- a/code/extensions/go/cgmanifest.json +++ b/code/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "fbdaec061157e98dda185c0ce771ce6a2c793045" + "commitHash": "415b7167f2e5396284b65692ef8fd08a3475362a" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.7.9" + "version": "0.8.0" } ], "version": 1 diff --git a/code/extensions/go/syntaxes/go.tmLanguage.json b/code/extensions/go/syntaxes/go.tmLanguage.json index db17cad3f91..48aa1d7264b 100644 --- a/code/extensions/go/syntaxes/go.tmLanguage.json +++ b/code/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/fbdaec061157e98dda185c0ce771ce6a2c793045", + "version": "https://github.com/worlpaker/go-syntax/commit/415b7167f2e5396284b65692ef8fd08a3475362a", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -618,6 +618,10 @@ { "match": "\\bany\\b", "name": "entity.name.type.any.go" + }, + { + "match": "\\bcomparable\\b", + "name": "entity.name.type.comparable.go" } ] }, @@ -1757,7 +1761,7 @@ "include": "#after_control_variables" }, { - "match": "(\\b[\\w\\.]+)(\\[(?:[^\\]]+)?\\])?(?=\\{)(? { + return runSafe(runtime, async () => { + for (const languageMode of languageModes.getAllModes()) { + const content = await languageMode.getTextDocumentContent?.(params.uri); + if (content) { + return { text: content }; + } + } + return null; + }, null, `Error while computing text document content for ${params.uri}`, token); + }); + // Listen on the connection connection.listen(); } diff --git a/code/extensions/html-language-features/server/src/modes/embeddedSupport.ts b/code/extensions/html-language-features/server/src/modes/embeddedSupport.ts index a6874e043b4..db378dce848 100644 --- a/code/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/code/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -56,9 +56,10 @@ export function getDocumentRegions(languageService: LanguageService, document: T } importedScripts.push(value); } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') { - if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) { + const token = scanner.getTokenText(); + if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === 'module') { languageIdFromType = 'javascript'; - } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) { + } else if (/["']text\/typescript["']/.test(token)) { languageIdFromType = 'typescript'; } else { languageIdFromType = undefined; diff --git a/code/extensions/html-language-features/server/src/modes/javascriptMode.ts b/code/extensions/html-language-features/server/src/modes/javascriptMode.ts index d820810c920..aafb54a64b5 100644 --- a/code/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/code/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -8,7 +8,7 @@ import { SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange, - LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext, CompletionItemData, isCompletionItemData + LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext, CompletionItemData, isCompletionItemData, FILE_PROTOCOL, DocumentUri } from './languageModes'; import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings'; import { HTMLDocumentRegions } from './embeddedSupport'; @@ -77,18 +77,24 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) { } }; - return ts.createLanguageService(host); + return { + service: ts.createLanguageService(host), + loadLibrary: libs.loadLibrary, + }; }); return { async getLanguageService(jsDocument: TextDocument): Promise { currentTextDocument = jsDocument; - return jsLanguageService; + return (await jsLanguageService).service; }, getCompilationSettings() { return compilerOptions; }, + async loadLibrary(fileName: string) { + return (await jsLanguageService).loadLibrary(fileName); + }, dispose() { - jsLanguageService.then(s => s.dispose()); + jsLanguageService.then(s => s.service.dispose()); } }; } @@ -104,6 +110,8 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache d.fileName === jsDocument.uri).map(d => { - return { - uri: document.uri, - range: convertRange(jsDocument, d.textSpan) - }; - }); + return (await Promise.all(definition.map(async d => { + if (d.fileName === jsDocument.uri) { + return { + uri: document.uri, + range: convertRange(jsDocument, d.textSpan) + }; + } else { + const libUri = libParentUri + d.fileName; + const content = await host.loadLibrary(d.fileName); + if (!content) { + return undefined; + } + const libDocument = TextDocument.create(libUri, languageId, 1, content); + return { + uri: libUri, + range: convertRange(libDocument, d.textSpan) + }; + } + }))).filter(d => !!d); } return null; }, @@ -402,6 +423,12 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { + if (documentUri.startsWith(libParentUri)) { + return host.loadLibrary(documentUri.substring(libParentUri.length)); + } + return undefined; + }, dispose() { host.dispose(); jsDocuments.dispose(); diff --git a/code/extensions/html-language-features/server/src/modes/languageModes.ts b/code/extensions/html-language-features/server/src/modes/languageModes.ts index 803fa6c1c87..45d0b8fabe7 100644 --- a/code/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/code/extensions/html-language-features/server/src/modes/languageModes.ts @@ -14,7 +14,7 @@ import { Color, ColorInformation, ColorPresentation, WorkspaceEdit, WorkspaceFolder } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; +import { DocumentUri, TextDocument } from 'vscode-languageserver-textdocument'; import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache'; import { getCSSMode } from './cssMode'; @@ -34,7 +34,7 @@ export { export { ClientCapabilities, DocumentContext, LanguageService, HTMLDocument, HTMLFormatConfiguration, TokenType } from 'vscode-html-languageservice'; -export { TextDocument } from 'vscode-languageserver-textdocument'; +export { TextDocument, DocumentUri } from 'vscode-languageserver-textdocument'; export interface Settings { readonly css?: any; @@ -89,6 +89,7 @@ export interface LanguageMode { onDocumentRemoved(document: TextDocument): void; getSemanticTokens?(document: TextDocument): Promise; getSemanticTokenLegend?(): { types: string[]; modifiers: string[] }; + getTextDocumentContent?(uri: DocumentUri): Promise; dispose(): void; } @@ -108,6 +109,8 @@ export interface LanguageModeRange extends Range { attributeValue?: boolean; } +export const FILE_PROTOCOL = 'html-server'; + export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: FileSystemProvider): LanguageModes { const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService }); const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService }); diff --git a/code/extensions/html/build/update-grammar.mjs b/code/extensions/html/build/update-grammar.mjs index 64bfe548faa..29934012ac4 100644 --- a/code/extensions/html/build/update-grammar.mjs +++ b/code/extensions/html/build/update-grammar.mjs @@ -32,6 +32,22 @@ function patchGrammar(grammar) { console.warn(`Expected to patch 2 occurrences of source.js & source.css: Was ${patchCount}`); } + return grammar; +} + +function patchGrammarDerivative(grammar) { + let patchCount = 0; + + let patterns = grammar.patterns; + for (let key in patterns) { + if (patterns[key]?.name === 'meta.tag.other.unrecognized.html.derivative' && patterns[key]?.begin === '(]*)(?]*)(? patchGrammar(grammar)); +const grammarDerivativePath = 'Syntaxes/HTML%20%28Derivative%29.tmLanguage'; +vscodeGrammarUpdater.update(tsGrammarRepo, grammarDerivativePath, './syntaxes/html-derivative.tmLanguage.json', grammar => patchGrammarDerivative(grammar)); diff --git a/code/extensions/html/syntaxes/html-derivative.tmLanguage.json b/code/extensions/html/syntaxes/html-derivative.tmLanguage.json index dc73025b9dd..f06c4d0ec44 100644 --- a/code/extensions/html/syntaxes/html-derivative.tmLanguage.json +++ b/code/extensions/html/syntaxes/html-derivative.tmLanguage.json @@ -23,7 +23,7 @@ "include": "text.html.basic#core-minus-invalid" }, { - "begin": "(]*)(?]*)(? { const uri = Uri.parse(uriPath); - const uriString = uri.toString(); + const uriString = uri.toString(true); if (uri.scheme === 'untitled') { throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); } diff --git a/code/extensions/json-language-features/server/package-lock.json b/code/extensions/json-language-features/server/package-lock.json index 00361defa5c..a0b95ddd48f 100644 --- a/code/extensions/json-language-features/server/package-lock.json +++ b/code/extensions/json-language-features/server/package-lock.json @@ -12,9 +12,9 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.4", + "vscode-json-languageservice": "^5.5.0", "vscode-languageserver": "^10.0.0-next.12", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "bin": { "vscode-json-languageserver": "bin/vscode-json-languageserver" @@ -64,9 +64,9 @@ "dev": true }, "node_modules/vscode-json-languageservice": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.4.tgz", - "integrity": "sha512-dgT16da8VznFv0IrEpBSKYvi29gxnMf5EOq+UfZSPaCiLZ65kgVOo3vMJSPNbZK8557YYbQH/fpMxxa4wRPAQw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.5.0.tgz", + "integrity": "sha512-JchBzp8ArzhCVpRS/LT4wzEEvwHXIUEdZD064cGTI4RVs34rNCZXPUguIYSfGBcHH1GV79ufPcfy3Pd8+ukbKw==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", diff --git a/code/extensions/json-language-features/server/package.json b/code/extensions/json-language-features/server/package.json index f503a7e1225..55deaff19e5 100644 --- a/code/extensions/json-language-features/server/package.json +++ b/code/extensions/json-language-features/server/package.json @@ -15,9 +15,9 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.4", + "vscode-json-languageservice": "^5.5.0", "vscode-languageserver": "^10.0.0-next.12", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" }, "devDependencies": { "@types/mocha": "^9.1.1", diff --git a/code/extensions/json-language-features/server/src/jsonServer.ts b/code/extensions/json-language-features/server/src/jsonServer.ts index 6a806b72df6..830ee8c4393 100644 --- a/code/extensions/json-language-features/server/src/jsonServer.ts +++ b/code/extensions/json-language-features/server/src/jsonServer.ts @@ -80,6 +80,8 @@ export interface RuntimeEnvironment { }; } +const sortCodeActionKind = CodeActionKind.Source.concat('.sort', '.json'); + export function startServer(connection: Connection, runtime: RuntimeEnvironment) { function getSchemaRequestService(handledSchemas: string[] = ['https', 'http', 'file']) { @@ -194,7 +196,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) interFileDependencies: false, workspaceDiagnostics: false }, - codeActionProvider: true + codeActionProvider: { + codeActionKinds: [sortCodeActionKind] + } }; return { capabilities }; @@ -446,7 +450,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return runSafeAsync(runtime, async () => { const document = documents.get(codeActionParams.textDocument.uri); if (document) { - const sortCodeAction = CodeAction.create('Sort JSON', CodeActionKind.Source.concat('.sort', '.json')); + const sortCodeAction = CodeAction.create('Sort JSON', sortCodeActionKind); sortCodeAction.command = { command: 'json.sort', title: l10n.t('Sort JSON') diff --git a/code/extensions/latex/cgmanifest.json b/code/extensions/latex/cgmanifest.json index d937ba4f430..eb4e0384157 100644 --- a/code/extensions/latex/cgmanifest.json +++ b/code/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "df6ef817c932d24da5cc72927344a547e463cc65" + "commitHash": "b46aaf9bf4d265e63e262ded4bf9beffe19d35b2" } }, "license": "MIT", - "version": "1.9.0", + "version": "1.13.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/code/extensions/latex/syntaxes/Bibtex.tmLanguage.json b/code/extensions/latex/syntaxes/Bibtex.tmLanguage.json index 0f3a3a408a5..ed523fcbdca 100644 --- a/code/extensions/latex/syntaxes/Bibtex.tmLanguage.json +++ b/code/extensions/latex/syntaxes/Bibtex.tmLanguage.json @@ -4,166 +4,146 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/c787db94a56bd93131ce0938046063320a02cc73", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/0fcf9283828cab2aa611072f54feb1e7d501c2b4", "name": "BibTeX", "scopeName": "text.bibtex", - "comment": "Grammar based on description from https://github.com/aclements/biblib\n", + "comment": "Grammar based on description from https://github.com/aclements/biblib", "patterns": [ { + "match": "@(?i:comment)(?=[\\s{(])", "captures": { "0": { "name": "punctuation.definition.comment.bibtex" } }, - "match": "@(?i:comment)(?=[\\s{(])", "name": "comment.block.at-sign.bibtex" }, { - "begin": "((@)(?i:preamble))\\s*(\\{)\\s*", - "beginCaptures": { - "1": { - "name": "keyword.other.preamble.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.preamble.begin.bibtex" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.section.preamble.end.bibtex" - } - }, - "name": "meta.preamble.braces.bibtex", - "patterns": [ - { - "include": "#field_value" - } - ] + "include": "#preamble" }, { - "begin": "((@)(?i:preamble))\\s*(\\()\\s*", - "beginCaptures": { - "1": { - "name": "keyword.other.preamble.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.preamble.begin.bibtex" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "punctuation.section.preamble.end.bibtex" - } - }, - "name": "meta.preamble.parenthesis.bibtex", - "patterns": [ - { - "include": "#field_value" - } - ] + "include": "#string" }, { - "begin": "((@)(?i:string))\\s*(\\{)\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", - "beginCaptures": { - "1": { - "name": "keyword.other.string-constant.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.string-constant.begin.bibtex" - }, - "4": { - "name": "variable.other.bibtex" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.section.string-constant.end.bibtex" - } - }, - "name": "meta.string-constant.braces.bibtex", - "patterns": [ - { - "include": "#field_value" - } - ] + "include": "#entry" }, { - "begin": "((@)(?i:string))\\s*(\\()\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", - "beginCaptures": { - "1": { - "name": "keyword.other.string-constant.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.string-constant.begin.bibtex" - }, - "4": { - "name": "variable.other.bibtex" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "punctuation.section.string-constant.end.bibtex" - } - }, - "name": "meta.string-constant.parenthesis.bibtex", + "begin": "[^@\\n]", + "end": "(?=@)", + "name": "comment.block.bibtex" + } + ], + "repository": { + "preamble": { "patterns": [ { - "include": "#field_value" + "begin": "((@)(?i:preamble))\\s*(\\{)\\s*", + "beginCaptures": { + "1": { + "name": "keyword.other.preamble.bibtex" + }, + "2": { + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.preamble.begin.bibtex" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.section.preamble.end.bibtex" + } + }, + "name": "meta.preamble.braces.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] + }, + { + "begin": "((@)(?i:preamble))\\s*(\\()\\s*", + "beginCaptures": { + "1": { + "name": "keyword.other.preamble.bibtex" + }, + "2": { + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.preamble.begin.bibtex" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.preamble.end.bibtex" + } + }, + "name": "meta.preamble.parenthesis.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] } ] }, - { - "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\{)\\s*([^\\s,}]*)", - "beginCaptures": { - "1": { - "name": "keyword.other.entry-type.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.entry.begin.bibtex" - }, - "4": { - "name": "entity.name.type.entry-key.bibtex" - } - }, - "end": "\\}", - "endCaptures": { - "0": { - "name": "punctuation.section.entry.end.bibtex" - } - }, - "name": "meta.entry.braces.bibtex", + "string": { "patterns": [ { - "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", + "begin": "((@)(?i:string))\\s*(\\{)\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", + "beginCaptures": { + "1": { + "name": "keyword.other.string-constant.bibtex" + }, + "2": { + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.string-constant.begin.bibtex" + }, + "4": { + "name": "variable.other.bibtex" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.section.string-constant.end.bibtex" + } + }, + "name": "meta.string-constant.braces.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] + }, + { + "begin": "((@)(?i:string))\\s*(\\()\\s*([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)", "beginCaptures": { "1": { - "name": "support.function.key.bibtex" + "name": "keyword.other.string-constant.bibtex" }, "2": { - "name": "punctuation.separator.key-value.bibtex" + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.string-constant.begin.bibtex" + }, + "4": { + "name": "variable.other.bibtex" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.string-constant.end.bibtex" } }, - "end": "(?=[,}])", - "name": "meta.key-assignment.bibtex", + "name": "meta.string-constant.parenthesis.bibtex", "patterns": [ { "include": "#field_value" @@ -172,57 +152,98 @@ } ] }, - { - "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\()\\s*([^\\s,]*)", - "beginCaptures": { - "1": { - "name": "keyword.other.entry-type.bibtex" - }, - "2": { - "name": "punctuation.definition.keyword.bibtex" - }, - "3": { - "name": "punctuation.section.entry.begin.bibtex" - }, - "4": { - "name": "entity.name.type.entry-key.bibtex" - } - }, - "end": "\\)", - "endCaptures": { - "0": { - "name": "punctuation.section.entry.end.bibtex" - } - }, - "name": "meta.entry.parenthesis.bibtex", + "entry": { "patterns": [ { - "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", + "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\{)\\s*([^\\s,}]*)", "beginCaptures": { "1": { - "name": "support.function.key.bibtex" + "name": "keyword.other.entry-type.bibtex" }, "2": { - "name": "punctuation.separator.key-value.bibtex" + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.entry.begin.bibtex" + }, + "4": { + "name": "entity.name.type.entry-key.bibtex" } }, - "end": "(?=[,)])", - "name": "meta.key-assignment.bibtex", + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.section.entry.end.bibtex" + } + }, + "name": "meta.entry.braces.bibtex", "patterns": [ { - "include": "#field_value" + "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", + "beginCaptures": { + "1": { + "name": "support.function.key.bibtex" + }, + "2": { + "name": "punctuation.separator.key-value.bibtex" + } + }, + "end": "(?=[,}])", + "name": "meta.key-assignment.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] + } + ] + }, + { + "begin": "((@)[a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\()\\s*([^\\s,]*)", + "beginCaptures": { + "1": { + "name": "keyword.other.entry-type.bibtex" + }, + "2": { + "name": "punctuation.definition.keyword.bibtex" + }, + "3": { + "name": "punctuation.section.entry.begin.bibtex" + }, + "4": { + "name": "entity.name.type.entry-key.bibtex" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.entry.end.bibtex" + } + }, + "name": "meta.entry.parenthesis.bibtex", + "patterns": [ + { + "begin": "([a-zA-Z!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~][a-zA-Z0-9!$&*+\\-./:;<>?@\\[\\\\\\]^_`|~]*)\\s*(\\=)", + "beginCaptures": { + "1": { + "name": "support.function.key.bibtex" + }, + "2": { + "name": "punctuation.separator.key-value.bibtex" + } + }, + "end": "(?=[,)])", + "name": "meta.key-assignment.bibtex", + "patterns": [ + { + "include": "#field_value" + } + ] } ] } ] }, - { - "begin": "[^@\\n]", - "end": "(?=@)", - "name": "comment.block.bibtex" - } - ], - "repository": { "field_value": { "patterns": [ { diff --git a/code/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/code/extensions/latex/syntaxes/LaTeX.tmLanguage.json index 06c4c59c60e..5a15e0eb15f 100644 --- a/code/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/code/extensions/latex/syntaxes/LaTeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/7b75bae583f3f9802c533e021f882428872c572c", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/a39a1f5ec1dee1c7e6e564ea86ab2c8d8779aa07", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -121,7 +121,7 @@ ] }, { - "begin": "((?:\\s*)\\\\begin\\{songs\\}\\{.*\\})", + "begin": "(\\s*\\\\begin\\{songs\\}\\{.*\\})", "captures": { "1": { "patterns": [ @@ -136,21 +136,45 @@ "name": "meta.function.environment.songs.latex", "patterns": [ { - "begin": "\\\\\\[", - "end": "\\]", - "name": "meta.chord.block.latex support.class.chord.block.environment.latex", - "patterns": [ - { - "include": "$self" - } - ] + "include": "text.tex.latex#songs-chords" + } + ] + }, + { + "comment": "This scope applies songs-environment coloring between \\\\beginsong and \\\\endsong. Useful in separate files without \\\\begin{songs}.", + "begin": "\\s*((\\\\)beginsong)(?=\\{)", + "captures": { + "1": { + "name": "support.function.be.latex" }, + "2": { + "name": "punctuation.definition.function.latex" + }, + "3": { + "name": "punctuation.definition.arguments.begin.latex" + }, + "4": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "end": "((\\\\)endsong)(?:\\s*\\n)?", + "name": "meta.function.environment.song.latex", + "patterns": [ { - "match": "\\^", - "name": "meta.chord.block.latex support.class.chord.block.environment.latex" + "include": "#multiline-arg-no-highlight" }, { - "include": "$self" + "include": "#multiline-optional-arg-no-highlight" + }, + { + "begin": "(?:\\G|(?<=\\]|\\}))\\s*", + "end": "\\s*(?=\\\\endsong)", + "contentName": "meta.data.environment.song.latex", + "patterns": [ + { + "include": "text.tex.latex#songs-chords" + } + ] } ] }, @@ -2198,7 +2222,7 @@ "include": "#definition-label" }, { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -2232,7 +2256,7 @@ "include": "#definition-label" }, { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -2349,11 +2373,11 @@ ] } }, - "contentName": "meta.embedded.internal_only_markdown_latex_combined", + "contentName": "meta.embedded.markdown_latex_combined", "end": "(\\\\end\\{markdown\\})", "patterns": [ { - "include": "text.tex.internal_only_markdown_latex_combined" + "include": "text.tex.markdown_latex_combined" } ] }, @@ -2949,7 +2973,7 @@ "name": "meta.function.verb.latex" }, { - "begin": "((\\\\)(?:directlua|luadirect))(\\{)", + "begin": "((\\\\)(?:directlua|luadirect|luaexec))(\\{)", "beginCaptures": { "1": { "name": "support.function.verb.latex" @@ -2994,7 +3018,7 @@ "name": "meta.math.block.latex support.class.math.block.environment.latex", "patterns": [ { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -3021,7 +3045,7 @@ "name": "constant.character.escape.latex" }, { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -3048,7 +3072,7 @@ "name": "constant.character.escape.latex" }, { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -3071,7 +3095,7 @@ "name": "meta.math.block.latex support.class.math.block.environment.latex", "patterns": [ { - "include": "text.tex#math" + "include": "text.tex#math-content" }, { "include": "$self" @@ -3250,7 +3274,7 @@ ] }, "multiline-optional-arg-no-highlight": { - "begin": "\\G\\[", + "begin": "(?:\\G|(?<=\\}))\\s*\\[", "beginCaptures": { "0": { "name": "punctuation.definition.arguments.optional.begin.latex" @@ -3269,6 +3293,26 @@ } ] }, + "multiline-arg-no-highlight": { + "begin": "\\G\\{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.arguments.begin.latex" + } + }, + "end": "\\}", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.latex" + } + }, + "name": "meta.parameter.latex", + "patterns": [ + { + "include": "$self" + } + ] + }, "optional-arg-bracket": { "patterns": [ { @@ -3354,6 +3398,27 @@ "name": "meta.parameter.optional.latex" } ] + }, + "songs-chords": { + "patterns": [ + { + "begin": "\\\\\\[", + "end": "\\]", + "name": "meta.chord.block.latex support.class.chord.block.environment.latex", + "patterns": [ + { + "include": "$self" + } + ] + }, + { + "match": "\\^", + "name": "meta.chord.block.latex support.class.chord.block.environment.latex" + }, + { + "include": "$self" + } + ] } } } \ No newline at end of file diff --git a/code/extensions/latex/syntaxes/TeX.tmLanguage.json b/code/extensions/latex/syntaxes/TeX.tmLanguage.json index b3a32817482..db2a62a2267 100644 --- a/code/extensions/latex/syntaxes/TeX.tmLanguage.json +++ b/code/extensions/latex/syntaxes/TeX.tmLanguage.json @@ -4,52 +4,40 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/df6ef817c932d24da5cc72927344a547e463cc65", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/b46aaf9bf4d265e63e262ded4bf9beffe19d35b2", "name": "TeX", "scopeName": "text.tex", "patterns": [ { - "begin": "(?<=^\\s*)((\\\\)iffalse)(?!\\s*[{}]\\s*\\\\fi)", - "beginCaptures": { - "1": { - "name": "keyword.control.tex" - }, - "2": { - "name": "punctuation.definition.keyword.tex" - } - }, - "contentName": "comment.line.percentage.tex", - "end": "((\\\\)(?:else|fi))", - "endCaptures": { - "1": { - "name": "keyword.control.tex" - }, - "2": { - "name": "punctuation.definition.keyword.tex" - } - }, - "patterns": [ - { - "include": "#comment" - }, - { - "include": "#braces" - }, - { - "include": "#conditionals" - } - ] + "include": "#iffalse-block" }, { - "captures": { - "1": { - "name": "punctuation.definition.keyword.tex" - } - }, - "match": "(\\\\)(backmatter|csname|else|endcsname|fi|frontmatter|mainmatter|unless|if(case|cat|csname|defined|dim|eof|false|fontchar|hbox|hmode|inner|mmode|num|odd|true|vbox|vmode|void|x)?)(?![a-zA-Z@])", - "name": "keyword.control.tex" + "include": "#macro-control" }, { + "include": "#catcode" + }, + { + "include": "#comment" + }, + { + "match": "[\\[\\]]", + "name": "punctuation.definition.brackets.tex" + }, + { + "include": "#dollar-math" + }, + { + "match": "\\\\\\\\", + "name": "keyword.control.newline.tex" + }, + { + "include": "#macro-general" + } + ], + "repository": { + "catcode": { + "match": "((\\\\)catcode)`(?:\\\\)?.(=)(\\d+)", "captures": { "1": { "name": "keyword.control.catcode.tex" @@ -64,85 +52,49 @@ "name": "constant.numeric.category.tex" } }, - "match": "((\\\\)catcode)`(?:\\\\)?.(=)(\\d+)", "name": "meta.catcode.tex" }, - { - "include": "#comment" - }, - { - "match": "[\\[\\]]", - "name": "punctuation.definition.brackets.tex" - }, - { - "begin": "(\\$\\$|\\$)", + "iffalse-block": { + "begin": "(?<=^\\s*)((\\\\)iffalse)(?!\\s*[{}]\\s*\\\\fi)", "beginCaptures": { "1": { - "name": "punctuation.definition.string.begin.tex" + "name": "keyword.control.tex" + }, + "2": { + "name": "punctuation.definition.keyword.tex" } }, - "end": "(\\1)", + "contentName": "comment.line.percentage.tex", + "end": "((\\\\)(?:else|fi))", "endCaptures": { "1": { - "name": "punctuation.definition.string.end.tex" + "name": "keyword.control.tex" + }, + "2": { + "name": "punctuation.definition.keyword.tex" } }, - "name": "meta.math.block.tex support.class.math.block.tex", "patterns": [ { - "match": "\\\\\\$", - "name": "constant.character.escape.tex" + "include": "#comment" }, { - "include": "#math" + "include": "#braces" }, { - "include": "$self" + "include": "#conditionals" } ] }, - { - "match": "\\\\\\\\", - "name": "keyword.control.newline.tex" - }, - { - "captures": { - "1": { - "name": "punctuation.definition.function.tex" - } - }, - "match": "(\\\\)_*[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", - "name": "support.class.general.latex3.tex" - }, - { - "captures": { - "1": { - "name": "punctuation.definition.function.tex" - } - }, - "match": "(\\.)[\\p{Alphabetic}@]+(?:_[\\p{Alphabetic}@]+)*:[NncVvoxefTFpwD]*", - "name": "support.class.general.latex3.tex" - }, - { - "captures": { - "1": { - "name": "punctuation.definition.function.tex" - } - }, - "match": "(\\\\)(?:[,;]|(?:[\\p{Alphabetic}@]+))", - "name": "support.function.general.tex" - }, - { + "macro-control": { + "match": "(\\\\)(backmatter|csname|else|endcsname|fi|frontmatter|mainmatter|unless|if(case|cat|csname|defined|dim|eof|false|fontchar|hbox|hmode|inner|mmode|num|odd|true|vbox|vmode|void|x)?)(?![a-zA-Z@])", "captures": { "1": { "name": "punctuation.definition.keyword.tex" } }, - "match": "(\\\\)[^a-zA-Z@]", - "name": "constant.character.escape.tex" - } - ], - "repository": { + "name": "keyword.control.tex" + }, "braces": { "begin": "(? ${1:${TM_SELECTED_TEXT}}", + "body": "${1:${TM_SELECTED_TEXT/^/> /gm}}", "description": "Insert quoted text" }, "Insert inline code": { diff --git a/code/extensions/media-preview/package.json b/code/extensions/media-preview/package.json index 7e2b70293fc..02b0134e4cf 100644 --- a/code/extensions/media-preview/package.json +++ b/code/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/code/extensions/media-preview/package.nls.json b/code/extensions/media-preview/package.nls.json index c45e1e2613b..920ced76435 100644 --- a/code/extensions/media-preview/package.nls.json +++ b/code/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/code/extensions/media-preview/src/audioPreview.ts b/code/extensions/media-preview/src/audioPreview.ts index e21a4189d7b..5058f7e978e 100644 --- a/code/extensions/media-preview/src/audioPreview.ts +++ b/code/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/code/extensions/media-preview/src/imagePreview/index.ts b/code/extensions/media-preview/src/imagePreview/index.ts index e0c605c2a6e..b405cd652c4 100644 --- a/code/extensions/media-preview/src/imagePreview/index.ts +++ b/code/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/code/extensions/media-preview/src/mediaPreview.ts b/code/extensions/media-preview/src/mediaPreview.ts index 26d1e25dbaa..ccf83166e29 100644 --- a/code/extensions/media-preview/src/mediaPreview.ts +++ b/code/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/code/extensions/media-preview/src/videoPreview.ts b/code/extensions/media-preview/src/videoPreview.ts index efc6be76a4f..67012128cf7 100644 --- a/code/extensions/media-preview/src/videoPreview.ts +++ b/code/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/code/extensions/merge-conflict/package-lock.json b/code/extensions/merge-conflict/package-lock.json index 5ee68d290f0..94caad8f57e 100644 --- a/code/extensions/merge-conflict/package-lock.json +++ b/code/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/code/extensions/microsoft-authentication/package-lock.json b/code/extensions/microsoft-authentication/package-lock.json index c688ccf9052..828a0b9a120 100644 --- a/code/extensions/microsoft-authentication/package-lock.json +++ b/code/extensions/microsoft-authentication/package-lock.json @@ -74,9 +74,9 @@ } }, "node_modules/@azure/msal-node-runtime": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.18.1.tgz", - "integrity": "sha512-vaUkpSiXD33/iDyZt1VZDEyOxvlNMT5o9D4ruIqkUmULyKgUik0y86DK2dsqZql/LU04T5siuq1AMTus+15SvA==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.18.2.tgz", + "integrity": "sha512-v45fyBQp80BrjZAeGJXl+qggHcbylQiFBihr0ijO2eniDCW9tz5TZBKYsqzH06VuiRaVG/Sa0Hcn4pjhJqFSTw==", "hasInstallScript": true, "license": "MIT" }, @@ -205,12 +205,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/code/extensions/microsoft-authentication/package.json b/code/extensions/microsoft-authentication/package.json index 16b8fa081e3..71d87ee271b 100644 --- a/code/extensions/microsoft-authentication/package.json +++ b/code/extensions/microsoft-authentication/package.json @@ -146,7 +146,7 @@ "vscode-tas-client": "^0.1.84" }, "overrides": { - "@azure/msal-node-runtime": "^0.18.1" + "@azure/msal-node-runtime": "^0.18.2" }, "repository": { "type": "git", diff --git a/code/extensions/microsoft-authentication/src/AADHelper.ts b/code/extensions/microsoft-authentication/src/AADHelper.ts index 9722145dd03..ad8dabe7533 100644 --- a/code/extensions/microsoft-authentication/src/AADHelper.ts +++ b/code/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/code/extensions/microsoft-authentication/src/common/loggerOptions.ts b/code/extensions/microsoft-authentication/src/common/loggerOptions.ts index d572f655f92..af5c1644a27 100644 --- a/code/extensions/microsoft-authentication/src/common/loggerOptions.ts +++ b/code/extensions/microsoft-authentication/src/common/loggerOptions.ts @@ -5,11 +5,15 @@ import { LogLevel as MsalLogLevel } from '@azure/msal-node'; import { env, LogLevel, LogOutputChannel } from 'vscode'; +import { MicrosoftAuthenticationTelemetryReporter } from './telemetryReporter'; export class MsalLoggerOptions { piiLoggingEnabled = false; - constructor(private readonly _output: LogOutputChannel) { } + constructor( + private readonly _output: LogOutputChannel, + private readonly _telemtryReporter: MicrosoftAuthenticationTelemetryReporter + ) { } get logLevel(): MsalLogLevel { return this._toMsalLogLevel(env.logLevel); @@ -27,6 +31,7 @@ export class MsalLoggerOptions { switch (level) { case MsalLogLevel.Error: this._output.error(message); + this._telemtryReporter.sendTelemetryErrorEvent(message); return; case MsalLogLevel.Warning: this._output.warn(message); diff --git a/code/extensions/microsoft-authentication/src/common/telemetryReporter.ts b/code/extensions/microsoft-authentication/src/common/telemetryReporter.ts index 25ac2623282..c28aa887e0c 100644 --- a/code/extensions/microsoft-authentication/src/common/telemetryReporter.ts +++ b/code/extensions/microsoft-authentication/src/common/telemetryReporter.ts @@ -66,6 +66,24 @@ export class MicrosoftAuthenticationTelemetryReporter implements IExperimentatio */ this._telemetryReporter.sendTelemetryEvent('logoutFailed'); } + + sendTelemetryErrorEvent(error: unknown): void { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const errorName = error instanceof Error ? error.name : undefined; + + /* __GDPR__ + "msalError" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine how often users run into issues with the login flow.", + "errorMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The error message from the exception." }, + "errorStack": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The stack trace from the exception." }, + "errorName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The name of the error." } + } + */ + this._telemetryReporter.sendTelemetryErrorEvent('msalError', { errorMessage, errorStack, errorName }); + } + /** * Sends an event for an account type available at startup. * @param scopes The scopes for the session diff --git a/code/extensions/microsoft-authentication/src/node/authProvider.ts b/code/extensions/microsoft-authentication/src/node/authProvider.ts index a26008fb780..5ce9acd3e6a 100644 --- a/code/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/code/extensions/microsoft-authentication/src/node/authProvider.ts @@ -86,7 +86,7 @@ export class MsalAuthProvider implements AuthenticationProvider { uriHandler: UriEventHandler, env: Environment = Environment.AzureCloud ): Promise { - const publicClientManager = await CachedPublicClientApplicationManager.create(context.secrets, logger, env.name); + const publicClientManager = await CachedPublicClientApplicationManager.create(context.secrets, logger, telemetryReporter, env.name); context.subscriptions.push(publicClientManager); const authProvider = new MsalAuthProvider(context, telemetryReporter, logger, uriHandler, publicClientManager, env); await authProvider.initialize(); @@ -354,6 +354,7 @@ export class MsalAuthProvider implements AuthenticationProvider { } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again + this._telemetryReporter.sendTelemetryErrorEvent(e); continue; } } @@ -368,7 +369,7 @@ export class MsalAuthProvider implements AuthenticationProvider { id: result.account?.homeAccountId ?? result.uniqueId, account: { id: result.account?.homeAccountId ?? result.uniqueId, - label: result.account?.username ?? 'Unknown', + label: result.account?.username.toLowerCase() ?? 'Unknown', }, scopes }; @@ -381,7 +382,7 @@ export class MsalAuthProvider implements AuthenticationProvider { scopes: [], account: { id: account.homeAccountId, - label: account.username + label: account.username.toLowerCase(), }, idToken: account.idToken, }; diff --git a/code/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/code/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index f0f4eb7b9bc..c1b4fbac4c1 100644 --- a/code/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/code/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -11,6 +11,7 @@ import { SecretStorageCachePlugin } from '../common/cachePlugin'; import { MsalLoggerOptions } from '../common/loggerOptions'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; import { IAccountAccess } from '../common/accountAccess'; +import { MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; export class CachedPublicClientApplication implements ICachedPublicClientApplication { // Core properties @@ -44,8 +45,9 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private readonly _secretStorage: SecretStorage, private readonly _accountAccess: IAccountAccess, private readonly _logger: LogOutputChannel, + telemetryReporter: MicrosoftAuthenticationTelemetryReporter ) { - const loggerOptions = new MsalLoggerOptions(_logger); + const loggerOptions = new MsalLoggerOptions(_logger, telemetryReporter); const nativeBrokerPlugin = new NativeBrokerPlugin(); this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable; this._pca = new PublicClientApplication({ @@ -75,9 +77,10 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica clientId: string, secretStorage: SecretStorage, accountAccess: IAccountAccess, - logger: LogOutputChannel + logger: LogOutputChannel, + telemetryReporter: MicrosoftAuthenticationTelemetryReporter ): Promise { - const app = new CachedPublicClientApplication(clientId, secretStorage, accountAccess, logger); + const app = new CachedPublicClientApplication(clientId, secretStorage, accountAccess, logger, telemetryReporter); await app.initialize(); return app; } diff --git a/code/extensions/microsoft-authentication/src/node/publicClientCache.ts b/code/extensions/microsoft-authentication/src/node/publicClientCache.ts index 16ccb80321f..777c3c5f272 100644 --- a/code/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/code/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -8,6 +8,7 @@ import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Eve import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; import { CachedPublicClientApplication } from './cachedPublicClientApplication'; import { IAccountAccess, ScopedAccountAccess } from '../common/accountAccess'; +import { MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; export interface IPublicClientApplicationInfo { clientId: string; @@ -29,6 +30,7 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient private readonly _accountAccess: IAccountAccess, private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel, + private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter, disposables: Disposable[] ) { this._disposable = Disposable.from( @@ -41,13 +43,14 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient static async create( secretStorage: SecretStorage, logger: LogOutputChannel, + telemetryReporter: MicrosoftAuthenticationTelemetryReporter, cloudName: string ): Promise { const pcasSecretStorage = await PublicClientApplicationsSecretStorage.create(secretStorage, cloudName); // TODO: Remove the migrations in a version const migrations = await pcasSecretStorage.getOldValue(); const accountAccess = await ScopedAccountAccess.create(secretStorage, cloudName, logger, migrations); - const manager = new CachedPublicClientApplicationManager(pcasSecretStorage, accountAccess, secretStorage, logger, [pcasSecretStorage, accountAccess]); + const manager = new CachedPublicClientApplicationManager(pcasSecretStorage, accountAccess, secretStorage, logger, telemetryReporter, [pcasSecretStorage, accountAccess]); await manager.initialize(); return manager; } @@ -138,7 +141,7 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } private async _doCreatePublicClientApplication(clientId: string): Promise { - const pca = await CachedPublicClientApplication.create(clientId, this._secretStorage, this._accountAccess, this._logger); + const pca = await CachedPublicClientApplication.create(clientId, this._secretStorage, this._accountAccess, this._logger, this._telemetryReporter); this._pcas.set(clientId, pca); const disposable = Disposable.from( pca, diff --git a/code/extensions/notebook-renderers/src/stackTraceHelper.ts b/code/extensions/notebook-renderers/src/stackTraceHelper.ts index ecf0eddb40e..9570ab8762d 100644 --- a/code/extensions/notebook-renderers/src/stackTraceHelper.ts +++ b/code/extensions/notebook-renderers/src/stackTraceHelper.ts @@ -11,6 +11,7 @@ export function formatStackTrace(stack: string): { formattedStack: string; error // Remove background colors. The ones from IPython don't work well with // themes 40-49 sets background color cleaned = stack.replace(/\u001b\[4\dm/g, ''); + cleaned = cleaned.replace(/(?<=\u001b\[[\d;]*?);4\d(?=m)/g, ''); // Also remove specific foreground colors (38 is the ascii code for picking one) (they don't translate either) // Turn them into default foreground diff --git a/code/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts b/code/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts index 54ec15b428c..faae56894f8 100644 --- a/code/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts +++ b/code/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts @@ -105,4 +105,13 @@ suite('StackTraceHelper', () => { formattedLines.slice(1).forEach(line => assert.ok(!//.test(line), 'line should not contain a link: ' + line)); }); + test('background (40-49) ANSI colors are removed', () => { + const stack = + 'open\u001b[39;49m\u001b[43m(\u001b[49m\u001b[33;43m\'\u001b[39;49m\u001b[33;43minput.txt\u001b[39;49m\u001b[33;43m\'\u001b[39;49m\u001b[43m)\u001b[49m;'; + + const formattedLines = formatStackTrace(stack).formattedStack.split('\n'); + assert.ok(!/4\d/.test(formattedLines[0]), 'should not contain background colors ' + formattedLines[0]); + formattedLines.slice(1).forEach(line => assert.ok(!//.test(line), 'line should not contain a link: ' + line)); + }); + }); diff --git a/code/extensions/package-lock.json b/code/extensions/package-lock.json index 18cb344614b..cc9030ab4ee 100644 --- a/code/extensions/package-lock.json +++ b/code/extensions/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "devDependencies": { "@parcel/watcher": "2.5.1", @@ -948,9 +948,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/code/extensions/package.json b/code/extensions/package.json index d45aaf69d13..0e28cace7e9 100644 --- a/code/extensions/package.json +++ b/code/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/code/extensions/php-language-features/src/features/phpGlobalFunctions.ts b/code/extensions/php-language-features/src/features/phpGlobalFunctions.ts index ab1c5487ae8..e8f10a29db5 100644 --- a/code/extensions/php-language-features/src/features/phpGlobalFunctions.ts +++ b/code/extensions/php-language-features/src/features/phpGlobalFunctions.ts @@ -1664,31 +1664,35 @@ export const globalfunctions: IEntries = { }, fclose: { description: 'Closes an open file pointer', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' + }, + fdatasync: { + description: 'Synchronizes data (but not meta-data) to the file', + signature: '( resource $stream ): bool' }, feof: { description: 'Tests for end-of-file on a file pointer', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' }, fflush: { description: 'Flushes the output to a file', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' }, fgetc: { description: 'Gets character from file pointer', - signature: '( resource $handle ): string' + signature: '( resource $string ): string|false' }, fgetcsv: { description: 'Gets line from file pointer and parse for CSV fields', - signature: '( resource $handle [, int $length = 0 [, string $delimiter = "," [, string $enclosure = \'"\' [, string $escape = "\\" ]]]]): array' + signature: '( resource $stream [, ?int $length = null [, string $separator = "," [, string $enclosure = \'"\' [, string $escape = "\\" ]]]]): array|false' }, fgets: { description: 'Gets line from file pointer', - signature: '( resource $handle [, int $length ]): string' + signature: '( resource $stream [, ?int $length = null ]): string|false' }, fgetss: { description: 'Gets line from file pointer and strip HTML tags', - signature: '( resource $handle [, int $length [, string $allowable_tags ]]): string' + signature: '( resource $handle [, int $length = ? [, string $allowable_tags = ? ]]): string' }, file_exists: { description: 'Checks whether a file or directory exists', @@ -1696,102 +1700,106 @@ export const globalfunctions: IEntries = { }, file_get_contents: { description: 'Reads entire file into a string', - signature: '( string $filename [, bool $use_include_path [, resource $context [, int $offset = 0 [, int $maxlen ]]]]): string' + signature: '( string $filename [, bool $use_include_path = false [, ?resource $context = null [, int $offset = 0 [, ?int $maxlen = null ]]]]): string|false' }, file_put_contents: { description: 'Write data to a file', - signature: '( string $filename , mixed $data [, int $flags = 0 [, resource $context ]]): int' + signature: '( string $filename , mixed $data [, int $flags = 0 [, ?resource $context = null ]]): int|false' }, file: { description: 'Reads entire file into an array', - signature: '( string $filename [, int $flags = 0 [, resource $context ]]): array' + signature: '( string $filename [, int $flags = 0 [, ?resource $context = null ]]): array|false' }, fileatime: { description: 'Gets last access time of file', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filectime: { description: 'Gets inode change time of file', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filegroup: { description: 'Gets file group', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileinode: { description: 'Gets file inode', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filemtime: { description: 'Gets file modification time', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileowner: { description: 'Gets file owner', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileperms: { description: 'Gets file permissions', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filesize: { description: 'Gets file size', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filetype: { description: 'Gets file type', - signature: '( string $filename ): string' + signature: '( string $filename ): string|false' }, flock: { description: 'Portable advisory file locking', - signature: '( resource $handle , int $operation [, int $wouldblock ]): bool' + signature: '( resource $stream , int $operation [, int &$would_block = null ]): bool' }, fnmatch: { description: 'Match filename against a pattern', - signature: '( string $pattern , string $string [, int $flags = 0 ]): bool' + signature: '( string $pattern , string $filename [, int $flags = 0 ]): bool' }, fopen: { description: 'Opens file or URL', - signature: '( string $filename , string $mode [, bool $use_include_path [, resource $context ]]): resource' + signature: '( string $filename , string $mode [, bool $use_include_path = false [, ?resource $context = null ]]): resource|false' }, fpassthru: { description: 'Output all remaining data on a file pointer', - signature: '( resource $handle ): int' + signature: '( resource $stream ): int' }, fputcsv: { description: 'Format line as CSV and write to file pointer', - signature: '( resource $handle , array $fields [, string $delimiter = "," [, string $enclosure = \'"\' [, string $escape_char = "\\" ]]]): int' + signature: '( resource $stream , array $fields [, string $separator = "," [, string $enclosure = \'"\' [, string $escape = "\\" [, string $eol = "\n" ]]]]): int|false' }, fputs: { description: 'Alias of fwrite', }, fread: { description: 'Binary-safe file read', - signature: '( resource $handle , int $length ): string' + signature: '( resource $stream , int $length ): string|false' }, fscanf: { description: 'Parses input from a file according to a format', - signature: '( resource $handle , string $format [, mixed $... ]): mixed' + signature: '( resource $stream , string $format [, mixed &...$vars ]): array|int|false|null' }, fseek: { description: 'Seeks on a file pointer', - signature: '( resource $handle , int $offset [, int $whence = SEEK_SET ]): int' + signature: '( resource $stream , int $offset [, int $whence = SEEK_SET ]): int' }, fstat: { description: 'Gets information about a file using an open file pointer', - signature: '( resource $handle ): array' + signature: '( resource $stream ): array|false' + }, + fsync: { + description: 'Synchronizes changes to the file (including meta-data)', + signature: '( resource $stream ): bool' }, ftell: { description: 'Returns the current position of the file read/write pointer', - signature: '( resource $handle ): int' + signature: '( resource $stream ): int|false' }, ftruncate: { description: 'Truncates a file to a given length', - signature: '( resource $handle , int $size ): bool' + signature: '( resource $stream , int $size ): bool' }, fwrite: { description: 'Binary-safe file write', - signature: '( resource $handle , string $string [, int $length ]): int' + signature: '( resource $stream , string $data [, ?int $length = null ]): int|false' }, glob: { description: 'Find pathnames matching a pattern', diff --git a/code/extensions/php/cgmanifest.json b/code/extensions/php/cgmanifest.json index 02faac2b0c0..7dd44bc830d 100644 --- a/code/extensions/php/cgmanifest.json +++ b/code/extensions/php/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/KapitanOczywisty/language-php", - "commitHash": "5e8f000cb5a20f44f7a7a89d07ad0774031c53f3" + "commitHash": "26cf1ebee89d4b55bf5823eb47eaa6a6dfda9336" } }, "license": "MIT", diff --git a/code/extensions/php/syntaxes/php.tmLanguage.json b/code/extensions/php/syntaxes/php.tmLanguage.json index 96821c6770c..63900f4c23a 100644 --- a/code/extensions/php/syntaxes/php.tmLanguage.json +++ b/code/extensions/php/syntaxes/php.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/KapitanOczywisty/language-php/commit/5e8f000cb5a20f44f7a7a89d07ad0774031c53f3", + "version": "https://github.com/KapitanOczywisty/language-php/commit/26cf1ebee89d4b55bf5823eb47eaa6a6dfda9336", "scopeName": "source.php", "patterns": [ { @@ -2402,23 +2402,68 @@ ] }, "instantiation": { - "begin": "(?i)(new)\\s+(?!class\\b)", - "beginCaptures": { - "1": { - "name": "keyword.other.new.php" - } - }, - "end": "(?i)(?=[^a-z0-9_\\x{7f}-\\x{10ffff}\\\\])", "patterns": [ { - "match": "(?i)(parent|static|self)(?![a-z0-9_\\x{7f}-\\x{10ffff}])", - "name": "storage.type.php" - }, - { - "include": "#class-name" + "match": "(?i)(new)\\s+(?!class\\b)([$a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)(?![a-z0-9_\\x{7f}-\\x{10ffff}\\\\(])", + "captures": { + "1": { + "name": "keyword.other.new.php" + }, + "2": { + "patterns": [ + { + "match": "(?i)(parent|static|self)(?![a-z0-9_\\x{7f}-\\x{10ffff}])", + "name": "storage.type.php" + }, + { + "include": "#class-name" + }, + { + "include": "#variable-name" + } + ] + } + } }, { - "include": "#variable-name" + "begin": "(?i)(new)\\s+(?!class\\b)([$a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "keyword.other.new.php" + }, + "2": { + "patterns": [ + { + "match": "(?i)(parent|static|self)(?![a-z0-9_\\x{7f}-\\x{10ffff}])", + "name": "storage.type.php" + }, + { + "include": "#class-name" + }, + { + "include": "#variable-name" + } + ] + }, + "3": { + "name": "punctuation.definition.arguments.begin.bracket.round.php" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.bracket.round.php" + } + }, + "contentName": "meta.function-call.php", + "patterns": [ + { + "include": "#named-arguments" + }, + { + "include": "$self" + } + ] } ] }, @@ -2784,6 +2829,10 @@ }, { "include": "#php_doc_types" + }, + { + "match": "[|&]", + "name": "punctuation.separator.delimiter.php" } ] }, @@ -2803,7 +2852,7 @@ ] }, "php_doc_types": { - "match": "(?i)\\??[a-z_\\x{7f}-\\x{10ffff}\\\\][a-z0-9_\\x{7f}-\\x{10ffff}\\\\]*([|&]\\??[a-z_\\x{7f}-\\x{10ffff}\\\\][a-z0-9_\\x{7f}-\\x{10ffff}\\\\]*)*", + "match": "(?i)\\??[a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+([|&]\\??[a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)*", "captures": { "0": { "patterns": [ @@ -2821,14 +2870,6 @@ { "match": "[|&]", "name": "punctuation.separator.delimiter.php" - }, - { - "match": "\\(", - "name": "punctuation.definition.type.begin.bracket.round.php" - }, - { - "match": "\\)", - "name": "punctuation.definition.type.end.bracket.round.php" } ] } @@ -2841,7 +2882,7 @@ "name": "punctuation.definition.type.begin.bracket.round.phpdoc.php" } }, - "end": "(\\))(\\[\\])|(?=\\*/)", + "end": "(\\))(\\[\\])?|(?=\\*/)", "endCaptures": { "1": { "name": "punctuation.definition.type.end.bracket.round.phpdoc.php" @@ -2867,7 +2908,7 @@ ] }, "php_doc_types_array_single": { - "match": "(?i)([a-z_\\x{7f}-\\x{10ffff}\\\\][a-z0-9_\\x{7f}-\\x{10ffff}\\\\]*)(\\[\\])", + "match": "(?i)([a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)(\\[\\])", "captures": { "1": { "patterns": [ diff --git a/code/extensions/prompt-basics/.vscodeignore b/code/extensions/prompt-basics/.vscodeignore new file mode 100644 index 00000000000..89fb2149dcb --- /dev/null +++ b/code/extensions/prompt-basics/.vscodeignore @@ -0,0 +1,4 @@ +test/** +src/** +tsconfig.json +cgmanifest.json diff --git a/code/extensions/prompt-basics/cgmanifest.json b/code/extensions/prompt-basics/cgmanifest.json new file mode 100644 index 00000000000..0c39c97297b --- /dev/null +++ b/code/extensions/prompt-basics/cgmanifest.json @@ -0,0 +1,4 @@ +{ + "registrations": [], + "version": 1 +} diff --git a/code/extensions/prompt-basics/language-configuration.json b/code/extensions/prompt-basics/language-configuration.json new file mode 100644 index 00000000000..935b1c66250 --- /dev/null +++ b/code/extensions/prompt-basics/language-configuration.json @@ -0,0 +1,103 @@ +{ + "comments": { + // symbols used for start and end a block comment. Remove this entry if your language does not support block comments + "blockComment": [ + "" + ] + }, + // symbols used as brackets + "brackets": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] + ], + "colorizedBracketPairs": [], + "autoClosingPairs": [ + { + "open": "{", + "close": "}" + }, + { + "open": "[", + "close": "]" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "<", + "close": ">", + "notIn": [ + "string" + ] + }, + ], + "surroundingPairs": [ + [ + "(", + ")" + ], + [ + "[", + "]" + ], + [ + "`", + "`" + ], + [ + "_", + "_" + ], + [ + "*", + "*" + ], + [ + "{", + "}" + ], + [ + "'", + "'" + ], + [ + "\"", + "\"" + ], + [ + "<", + ">" + ], + [ + "~", + "~" + ], + [ + "$", + "$" + ] + ], + "folding": { + "offSide": true, + "markers": { + "start": "^\\s*", + "end": "^\\s*" + } + }, + "wordPattern": { + "pattern": "(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})(((\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})|[_])?(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark}))*", + "flags": "ug" + }, +} diff --git a/code/extensions/prompt-basics/package.json b/code/extensions/prompt-basics/package.json new file mode 100644 index 00000000000..7957cdd4d0e --- /dev/null +++ b/code/extensions/prompt-basics/package.json @@ -0,0 +1,87 @@ +{ + "name": "prompt", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.20.0" + }, + "categories": ["Programming Languages"], + "contributes": { + "languages": [ + { + "id": "prompt", + "aliases": [ + "Prompt", + "prompt" + ], + "extensions": [ + ".prompt.md", + "copilot-instructions.md" + ], + "configuration": "./language-configuration.json" + }, + { + "id": "instructions", + "aliases": [ + "Instructions", + "instructions" + ], + "extensions": [ + ".instructions.md", + "copilot-instructions.md" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "prompt", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] + }, + { + "language": "instructions", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] + } + ], + "configurationDefaults": { + "[prompt]": { + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false + }, + "[instructions]": { + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false + } + }, + "snippets": [ + { + "language": "prompt", + "path": "./snippets/prompt.code-snippets" + }, + { + "language": "instructions", + "path": "./snippets/instructions.code-snippets" + } + ] + }, + "scripts": {}, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/code/extensions/prompt-basics/package.nls.json b/code/extensions/prompt-basics/package.nls.json new file mode 100644 index 00000000000..207593c43c8 --- /dev/null +++ b/code/extensions/prompt-basics/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Prompt Language Basics", + "description": "Syntax highlighting for Prompt and Instructions documents." +} diff --git a/code/extensions/prompt-basics/snippets/instructions.code-snippets b/code/extensions/prompt-basics/snippets/instructions.code-snippets new file mode 100644 index 00000000000..89f3046c80e --- /dev/null +++ b/code/extensions/prompt-basics/snippets/instructions.code-snippets @@ -0,0 +1,13 @@ +{ + "fileTemplate": { + "prefix": "New Chat Instructions", + "body": [ + "---", + "applyTo: '${1|**,**/*.ts|}'", + "---", + "${2:Coding standards, domain knowledge, and preferences that AI should follow.}", + ], + "description": "Instructions guide and customize Chat behavior by providing context, coding standards, and preferences.", + "isFileTemplate": true, + } +} diff --git a/code/extensions/prompt-basics/snippets/prompt.code-snippets b/code/extensions/prompt-basics/snippets/prompt.code-snippets new file mode 100644 index 00000000000..51e456acc25 --- /dev/null +++ b/code/extensions/prompt-basics/snippets/prompt.code-snippets @@ -0,0 +1,13 @@ +{ + "fileTemplate": { + "prefix": "New Reusable Chat Prompt", + "body": [ + "---", + "mode: '${1|ask,edit,agent|}'", + "---", + "${2:Expected output and any relevant constraints for this task.}", + ], + "description": "Prompts define reusable and task-specific steps for automating Chat workflows.", + "isFileTemplate": true, + } +} diff --git a/code/extensions/prompt-basics/syntaxes/prompt.tmLanguage.json b/code/extensions/prompt-basics/syntaxes/prompt.tmLanguage.json new file mode 100644 index 00000000000..314dc26aaed --- /dev/null +++ b/code/extensions/prompt-basics/syntaxes/prompt.tmLanguage.json @@ -0,0 +1,15 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/microsoft/vscode-markdown-tm-grammar/blob/master/syntaxes/markdown.tmLanguage", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "0.1.0", + "name": "Prompt", + "scopeName": "text.html.markdown.prompt", + "patterns": [ + { + "include": "text.html.markdown" + } + ] +} diff --git a/code/extensions/rust/cgmanifest.json b/code/extensions/rust/cgmanifest.json index a0eb585e052..73be467648b 100644 --- a/code/extensions/rust/cgmanifest.json +++ b/code/extensions/rust/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "rust-syntax", "repositoryUrl": "https://github.com/dustypomerleau/rust-syntax", - "commitHash": "e90d3dbdb61b96e4afdce6f7a3572426b1a86d9d" + "commitHash": "268fd42cfd4aa96a6ed9024a2850d17d6cd2dc7b" } }, "license": "MIT", diff --git a/code/extensions/rust/syntaxes/rust.tmLanguage.json b/code/extensions/rust/syntaxes/rust.tmLanguage.json index 16307e72a6a..5f871ae8895 100644 --- a/code/extensions/rust/syntaxes/rust.tmLanguage.json +++ b/code/extensions/rust/syntaxes/rust.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dustypomerleau/rust-syntax/commit/e90d3dbdb61b96e4afdce6f7a3572426b1a86d9d", + "version": "https://github.com/dustypomerleau/rust-syntax/commit/268fd42cfd4aa96a6ed9024a2850d17d6cd2dc7b", "name": "Rust", "scopeName": "source.rust", "patterns": [ @@ -52,7 +52,7 @@ { "comment": "macro type metavariables", "name": "meta.macro.metavariable.type.rust", - "match": "(\\$)((crate)|([A-Z][A-Za-z0-9_]*))((:)(block|expr|ident|item|lifetime|literal|meta|path?|stmt|tt|ty|vis))?", + "match": "(\\$)((crate)|([A-Z]\\w*))(\\s*(:)\\s*(block|expr(?:_2021)?|ident|item|lifetime|literal|meta|pat(?:_param)?|path|stmt|tt|ty|vis)\\b)?", "captures": { "1": { "name": "keyword.operator.macro.dollar.rust" @@ -79,7 +79,7 @@ { "comment": "macro metavariables", "name": "meta.macro.metavariable.rust", - "match": "(\\$)([a-z][A-Za-z0-9_]*)((:)(block|expr|ident|item|lifetime|literal|meta|path?|stmt|tt|ty|vis))?", + "match": "(\\$)([a-z]\\w*)(\\s*(:)\\s*(block|expr(?:_2021)?|ident|item|lifetime|literal|meta|pat(?:_param)?|path|stmt|tt|ty|vis)\\b)?", "captures": { "1": { "name": "keyword.operator.macro.dollar.rust" diff --git a/code/extensions/simple-browser/preview-src/index.ts b/code/extensions/simple-browser/preview-src/index.ts index 3d804aa60fa..d2b0b7549e9 100644 --- a/code/extensions/simple-browser/preview-src/index.ts +++ b/code/extensions/simple-browser/preview-src/index.ts @@ -95,6 +95,8 @@ onceDocumentLoaded(() => { // Try to bust the cache for the iframe // There does not appear to be any way to reliably do this except modifying the url + const existing = new URLSearchParams(location.search); + url.searchParams.append('id', existing.get('id')!); url.searchParams.append('vscodeBrowserReqId', Date.now().toString()); iframe.src = url.toString(); diff --git a/code/extensions/terminal-suggest/.vscodeignore b/code/extensions/terminal-suggest/.vscodeignore index 21df0d1f7ac..d9b5dc0447c 100644 --- a/code/extensions/terminal-suggest/.vscodeignore +++ b/code/extensions/terminal-suggest/.vscodeignore @@ -8,3 +8,4 @@ package-lock.json fixtures/** scripts/** testWorkspace/** +cgmanifest.json diff --git a/code/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts b/code/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts index dd54a2439ec..c0a3abc9000 100644 --- a/code/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts +++ b/code/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/code/extensions/terminal-suggest/src/completions/code-tunnel.ts b/code/extensions/terminal-suggest/src/completions/code-tunnel.ts index a53edd52ef2..6abeabb0db7 100644 --- a/code/extensions/terminal-suggest/src/completions/code-tunnel.ts +++ b/code/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/code/extensions/terminal-suggest/src/completions/code.ts b/code/extensions/terminal-suggest/src/completions/code.ts index 1ae5957ac61..a6dfd47738e 100644 --- a/code/extensions/terminal-suggest/src/completions/code.ts +++ b/code/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/code/extensions/terminal-suggest/src/terminalSuggestMain.ts b/code/extensions/terminal-suggest/src/terminalSuggestMain.ts index 8ca8f977144..382142f8aa3 100644 --- a/code/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/code/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -34,7 +34,8 @@ export const enum TerminalShellType { Fish = 'fish', Zsh = 'zsh', PowerShell = 'pwsh', - Python = 'python' + Python = 'python', + GitBash = 'gitbash', } const isWindows = osIsWindows(); @@ -141,8 +142,8 @@ export async function activate(context: vscode.ExtensionContext) { } } - if (result.cwd && (result.filesRequested || result.foldersRequested)) { - return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, fileExtensions: result.fileExtensions, cwd: result.cwd, env: terminal.shellIntegration?.env?.value }); + if (terminal.shellIntegration?.cwd && (result.filesRequested || result.foldersRequested)) { + return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, fileExtensions: result.fileExtensions, cwd: result.cwd ?? terminal.shellIntegration.cwd, env: terminal.shellIntegration?.env?.value }); } return result.items; } @@ -275,16 +276,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 +306,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)) { @@ -337,6 +323,8 @@ function getTerminalShellType(shellType: string | undefined): TerminalShellType switch (shellType) { case 'bash': return TerminalShellType.Bash; + case 'gitbash': + return TerminalShellType.GitBash; case 'zsh': return TerminalShellType.Zsh; case 'pwsh': diff --git a/code/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts b/code/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts index 447772ad8b7..f64688066e3 100644 --- a/code/extensions/terminal-suggest/src/test/completions/code-insiders.test.ts +++ b/code/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/code/extensions/terminal-suggest/src/test/completions/code.test.ts b/code/extensions/terminal-suggest/src/test/completions/code.test.ts index b327e3e438e..d00dcdbf947 100644 --- a/code/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/code/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/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index 69b65b2002c..85f9cb6ca98 100644 --- a/code/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/code/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/code/extensions/tunnel-forwarding/package-lock.json b/code/extensions/tunnel-forwarding/package-lock.json index 57accdc3b8d..307cef66071 100644 --- a/code/extensions/tunnel-forwarding/package-lock.json +++ b/code/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/code/extensions/typescript-basics/package.json b/code/extensions/typescript-basics/package.json index d765f6116f8..d64e6df2147 100644 --- a/code/extensions/typescript-basics/package.json +++ b/code/extensions/typescript-basics/package.json @@ -75,9 +75,7 @@ "keyword.operator.assignment.compound.bitwise.ts" ], "tokenTypes": { - "meta.template.expression": "other", - "meta.template.expression string": "string", - "meta.template.expression comment": "comment", + "punctuation.definition.template-expression": "other", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", @@ -102,9 +100,7 @@ "meta.embedded.expression.tsx": "typescriptreact" }, "tokenTypes": { - "meta.template.expression": "other", - "meta.template.expression string": "string", - "meta.template.expression comment": "comment", + "punctuation.definition.template-expression": "other", "entity.name.type.instance.jsdoc": "other", "entity.name.function.tagged-template": "other", "meta.import string.quoted": "other", diff --git a/code/extensions/typescript-language-features/package-lock.json b/code/extensions/typescript-language-features/package-lock.json index 4f97eb1048c..2078b4845e8 100644 --- a/code/extensions/typescript-language-features/package-lock.json +++ b/code/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/code/extensions/typescript-language-features/package.json b/code/extensions/typescript-language-features/package.json index 541313a81cf..5f39dc5f984 100644 --- a/code/extensions/typescript-language-features/package.json +++ b/code/extensions/typescript-language-features/package.json @@ -11,7 +11,8 @@ "workspaceTrust", "multiDocumentHighlightProvider", "codeActionAI", - "codeActionRanges" + "codeActionRanges", + "editorHoverVerbosityLevel" ], "capabilities": { "virtualWorkspaces": { @@ -147,1370 +148,1424 @@ "url": "https://typedoc.org/schema.json" } ], - "configuration": { - "type": "object", - "title": "%configuration.typescript%", - "order": 20, - "properties": { - "typescript.tsdk": { - "type": "string", - "markdownDescription": "%typescript.tsdk.desc%", - "scope": "window" - }, - "typescript.disableAutomaticTypeAcquisition": { - "type": "boolean", - "default": false, - "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", - "scope": "window", - "tags": [ - "usesOnlineServices" - ] - }, - "typescript.enablePromptUseWorkspaceTsdk": { - "type": "boolean", - "default": false, - "description": "%typescript.enablePromptUseWorkspaceTsdk%", - "scope": "window" - }, - "typescript.npm": { - "type": "string", - "markdownDescription": "%typescript.npm%", - "scope": "machine" - }, - "typescript.check.npmIsInstalled": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.check.npmIsInstalled%", - "scope": "window" - }, - "javascript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%javascript.referencesCodeLens.enabled%", - "scope": "window" - }, - "javascript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "description": "%javascript.referencesCodeLens.showOnAllFunctions%", - "scope": "window" - }, - "typescript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%typescript.referencesCodeLens.enabled%", - "scope": "window" - }, - "typescript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "description": "%typescript.referencesCodeLens.showOnAllFunctions%", - "scope": "window" - }, - "typescript.implementationsCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%typescript.implementationsCodeLens.enabled%", - "scope": "window" - }, - "typescript.implementationsCodeLens.showOnInterfaceMethods": { - "type": "boolean", - "default": false, - "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", - "scope": "window" - }, - "typescript.tsserver.enableTracing": { - "type": "boolean", - "default": false, - "description": "%typescript.tsserver.enableTracing%", - "scope": "window" - }, - "typescript.tsserver.log": { - "type": "string", - "enum": [ - "off", - "terse", - "normal", - "verbose" - ], - "default": "off", - "description": "%typescript.tsserver.log%", - "scope": "window" - }, - "typescript.tsserver.pluginPaths": { - "type": "array", - "items": { + "configuration": [ + { + "type": "object", + "order": 20, + "properties": { + "typescript.tsdk": { "type": "string", - "description": "%typescript.tsserver.pluginPaths.item%" + "markdownDescription": "%typescript.tsdk.desc%", + "scope": "window" }, - "default": [], - "description": "%typescript.tsserver.pluginPaths%", - "scope": "machine" - }, - "javascript.suggest.completeFunctionCalls": { - "type": "boolean", - "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "scope": "resource" - }, - "typescript.suggest.completeFunctionCalls": { - "type": "boolean", - "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "scope": "resource" - }, - "javascript.suggest.includeAutomaticOptionalChainCompletions": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "scope": "resource" - }, - "typescript.suggest.includeAutomaticOptionalChainCompletions": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.enabled": { - "type": "string", - "enum": [ - "none", - "literals", - "all" - ], - "enumDescriptions": [ - "%inlayHints.parameterNames.none%", - "%inlayHints.parameterNames.literals%", - "%inlayHints.parameterNames.all%" - ], - "default": "none", - "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.enumMemberValues.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.parameterNames.enabled": { - "type": "string", - "enum": [ - "none", - "literals", - "all" - ], - "enumDescriptions": [ - "%inlayHints.parameterNames.none%", - "%inlayHints.parameterNames.literals%", - "%inlayHints.parameterNames.all%" - ], - "default": "none", - "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "javascript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "javascript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "javascript.suggest.includeCompletionsForImportStatements": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "scope": "resource" - }, - "typescript.suggest.includeCompletionsForImportStatements": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "scope": "resource" - }, - "typescript.reportStyleChecksAsWarnings": { - "type": "boolean", - "default": true, - "description": "%typescript.reportStyleChecksAsWarnings%", - "scope": "window" - }, - "typescript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%typescript.validate.enable%", - "scope": "window" - }, - "typescript.format.enable": { - "type": "boolean", - "default": true, - "description": "%typescript.format.enable%", - "scope": "window" - }, - "typescript.format.insertSpaceAfterCommaDelimiter": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterConstructor": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterSemicolonInForStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "scope": "resource" - }, - "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "scope": "resource" - }, - "typescript.format.insertSpaceBeforeFunctionParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterTypeAssertion": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterTypeAssertion%", - "scope": "resource" - }, - "typescript.format.placeOpenBraceOnNewLineForFunctions": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "scope": "resource" - }, - "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "scope": "resource" - }, - "typescript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] - }, - "typescript.format.indentSwitchCase": { - "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "scope": "resource" - }, - "javascript.format.indentSwitchCase": { - "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "scope": "resource" - }, - "javascript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%javascript.validate.enable%", - "scope": "window" - }, - "javascript.format.enable": { - "type": "boolean", - "default": true, - "description": "%javascript.format.enable%", - "scope": "window" - }, - "javascript.format.insertSpaceAfterCommaDelimiter": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterConstructor": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterSemicolonInForStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "scope": "resource" - }, - "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "scope": "resource" - }, - "javascript.format.insertSpaceBeforeFunctionParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "scope": "resource" - }, - "javascript.format.placeOpenBraceOnNewLineForFunctions": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "scope": "resource" - }, - "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "scope": "resource" - }, - "javascript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] - }, - "js/ts.implicitProjectConfig.module": { - "type": "string", - "markdownDescription": "%configuration.implicitProjectConfig.module%", - "default": "ESNext", - "enum": [ - "CommonJS", - "AMD", - "System", - "UMD", - "ES6", - "ES2015", - "ES2020", - "ESNext", - "None", - "ES2022", - "Node12", - "NodeNext" - ], - "scope": "window" - }, - "js/ts.implicitProjectConfig.target": { - "type": "string", - "default": "ES2022", - "markdownDescription": "%configuration.implicitProjectConfig.target%", - "enum": [ - "ES3", - "ES5", - "ES6", - "ES2015", - "ES2016", - "ES2017", - "ES2018", - "ES2019", - "ES2020", - "ES2021", - "ES2022", - "ES2023", - "ES2024", - "ESNext" - ], - "scope": "window" - }, - "javascript.implicitProjectConfig.checkJs": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", - "markdownDeprecationMessage": "%configuration.javascript.checkJs.checkJs.deprecation%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.checkJs": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", - "scope": "window" - }, - "javascript.implicitProjectConfig.experimentalDecorators": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", - "markdownDeprecationMessage": "%configuration.javascript.checkJs.experimentalDecorators.deprecation%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.experimentalDecorators": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictNullChecks": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictFunctionTypes": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", - "scope": "window" - }, - "javascript.suggest.names": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.names%", - "scope": "resource" - }, - "typescript.tsc.autoDetect": { - "type": "string", - "default": "on", - "enum": [ - "on", - "off", - "build", - "watch" - ], - "markdownEnumDescriptions": [ - "%typescript.tsc.autoDetect.on%", - "%typescript.tsc.autoDetect.off%", - "%typescript.tsc.autoDetect.build%", - "%typescript.tsc.autoDetect.watch%" - ], - "description": "%typescript.tsc.autoDetect%", - "scope": "window" - }, - "javascript.suggest.paths": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.paths%", - "scope": "resource" - }, - "typescript.suggest.paths": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.paths%", - "scope": "resource" - }, - "javascript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "scope": "resource" - }, - "typescript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "scope": "resource" - }, - "javascript.suggest.completeJSDocs": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "scope": "language-overridable" - }, - "typescript.suggest.completeJSDocs": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "scope": "language-overridable" - }, - "javascript.suggest.jsdoc.generateReturns": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "scope": "language-overridable" - }, - "typescript.suggest.jsdoc.generateReturns": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "scope": "language-overridable" - }, - "typescript.locale": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "de", - "es", - "en", - "fr", - "it", - "ja", - "ko", - "ru", - "zh-CN", - "zh-TW" - ], - "enumDescriptions": [ - "%typescript.locale.auto%", - "Deutsch", - "español", - "English", - "français", - "italiano", - "日本語", - "한국어", - "русский", - "中文(简体)", - "中文(繁體)" - ], - "markdownDescription": "%typescript.locale%", - "scope": "window" - }, - "javascript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%javascript.suggestionActions.enabled%", - "scope": "resource" - }, - "typescript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggestionActions.enabled%", - "scope": "resource" - }, - "javascript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "scope": "language-overridable" - }, - "typescript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "scope": "language-overridable" - }, - "javascript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "scope": "language-overridable" - }, - "typescript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "scope": "language-overridable" - }, - "javascript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "scope": "language-overridable" - }, - "typescript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "scope": "language-overridable" - }, - "javascript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%javascript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "scope": "language-overridable" - }, - "typescript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "scope": "language-overridable" - }, - "typescript.preferences.includePackageJsonAutoImports": { - "type": "string", - "enum": [ - "auto", - "on", - "off" - ], - "enumDescriptions": [ - "%typescript.preferences.includePackageJsonAutoImports.auto%", - "%typescript.preferences.includePackageJsonAutoImports.on%", - "%typescript.preferences.includePackageJsonAutoImports.off%" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", - "scope": "window" - }, - "typescript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" + "typescript.disableAutomaticTypeAcquisition": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", + "scope": "window", + "tags": [ + "usesOnlineServices" + ] }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "scope": "resource" - }, - "javascript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" + "typescript.enablePromptUseWorkspaceTsdk": { + "type": "boolean", + "default": false, + "description": "%typescript.enablePromptUseWorkspaceTsdk%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "scope": "resource" - }, - "typescript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" + "javascript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%javascript.referencesCodeLens.enabled%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "scope": "resource" - }, - "javascript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" + "javascript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "%javascript.referencesCodeLens.showOnAllFunctions%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "scope": "resource" - }, - "typescript.preferences.preferTypeOnlyAutoImports": { - "type": "boolean", - "default": false, - "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", - "scope": "resource" - }, - "javascript.preferences.renameShorthandProperties": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", - "scope": "language-overridable" - }, - "typescript.preferences.renameShorthandProperties": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", - "scope": "language-overridable" - }, - "javascript.preferences.useAliasesForRenames": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "scope": "language-overridable" - }, - "typescript.preferences.useAliasesForRenames": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "scope": "language-overridable" - }, - "javascript.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "scope": "language-overridable" - }, - "typescript.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "scope": "language-overridable" - }, - "typescript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } + "typescript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.enabled%", + "scope": "window" + }, + "typescript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.showOnAllFunctions%", + "scope": "window" + }, + "typescript.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.enabled%", + "scope": "window" + }, + "typescript.experimental.useTsgo": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.useTsgo%", + "scope": "window", + "tags": [ + "experimental" + ] + }, + "typescript.implementationsCodeLens.showOnInterfaceMethods": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", + "scope": "window" + }, + "typescript.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true, + "description": "%typescript.reportStyleChecksAsWarnings%", + "scope": "window" + }, + "typescript.validate.enable": { + "type": "boolean", + "default": true, + "description": "%typescript.validate.enable%", + "scope": "window" + }, + "javascript.validate.enable": { + "type": "boolean", + "default": true, + "description": "%javascript.validate.enable%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.module": { + "type": "string", + "markdownDescription": "%configuration.implicitProjectConfig.module%", + "default": "ESNext", + "enum": [ + "CommonJS", + "AMD", + "System", + "UMD", + "ES6", + "ES2015", + "ES2020", + "ESNext", + "None", + "ES2022", + "Node12", + "NodeNext" + ], + "scope": "window" + }, + "js/ts.implicitProjectConfig.target": { + "type": "string", + "default": "ES2022", + "markdownDescription": "%configuration.implicitProjectConfig.target%", + "enum": [ + "ES3", + "ES5", + "ES6", + "ES2015", + "ES2016", + "ES2017", + "ES2018", + "ES2019", + "ES2020", + "ES2021", + "ES2022", + "ES2023", + "ES2024", + "ESNext" + ], + "scope": "window" + }, + "javascript.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", + "markdownDeprecationMessage": "%configuration.javascript.checkJs.checkJs.deprecation%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", + "scope": "window" + }, + "javascript.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", + "markdownDeprecationMessage": "%configuration.javascript.checkJs.experimentalDecorators.deprecation%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictNullChecks": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictFunctionTypes": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", + "scope": "window" + }, + "typescript.tsc.autoDetect": { + "type": "string", + "default": "on", + "enum": [ + "on", + "off", + "build", + "watch" + ], + "markdownEnumDescriptions": [ + "%typescript.tsc.autoDetect.on%", + "%typescript.tsc.autoDetect.off%", + "%typescript.tsc.autoDetect.build%", + "%typescript.tsc.autoDetect.watch%" + ], + "description": "%typescript.tsc.autoDetect%", + "scope": "window" + }, + "typescript.locale": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "de", + "es", + "en", + "fr", + "it", + "ja", + "ko", + "ru", + "zh-CN", + "zh-TW" + ], + "enumDescriptions": [ + "%typescript.locale.auto%", + "Deutsch", + "español", + "English", + "français", + "italiano", + "日本語", + "한국어", + "русский", + "中文(简体)", + "中文(繁體)" + ], + "markdownDescription": "%typescript.locale%", + "scope": "window" + }, + "javascript.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%javascript.suggestionActions.enabled%", + "scope": "resource" + }, + "typescript.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggestionActions.enabled%", + "scope": "resource" + }, + "typescript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" + }, + "javascript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" + }, + "typescript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%", + "scope": "language-overridable" + }, + "javascript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%", + "scope": "language-overridable" + }, + "typescript.workspaceSymbols.scope": { + "type": "string", + "enum": [ + "allOpenProjects", + "currentProject" + ], + "enumDescriptions": [ + "%typescript.workspaceSymbols.scope.allOpenProjects%", + "%typescript.workspaceSymbols.scope.currentProject%" + ], + "default": "allOpenProjects", + "markdownDescription": "%typescript.workspaceSymbols.scope%", + "scope": "window" + }, + "typescript.preferGoToSourceDefinition": { + "type": "boolean", + "default": false, + "description": "%configuration.preferGoToSourceDefinition%", + "scope": "window" + }, + "javascript.preferGoToSourceDefinition": { + "type": "boolean", + "default": false, + "description": "%configuration.preferGoToSourceDefinition%", + "scope": "window" + }, + "typescript.workspaceSymbols.excludeLibrarySymbols": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "scope": "window" + }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, + "javascript.updateImportsOnPaste.enabled": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" + }, + "typescript.updateImportsOnPaste.enabled": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" + }, + "typescript.experimental.expandableHover": { + "type": "boolean", + "default": true, + "description": "%configuration.expandableHover%", + "scope": "window", + "tags": [ + "experimental" + ] } - }, - "javascript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" + } + }, + { + "type": "object", + "title": "%configuration.suggest%", + "order": 21, + "properties": { + "javascript.suggest.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggest.enabled%", + "scope": "language-overridable" + }, + "typescript.suggest.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggest.enabled%", + "scope": "language-overridable" + }, + "javascript.suggest.autoImports": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.autoImports%", + "scope": "resource" + }, + "typescript.suggest.autoImports": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.autoImports%", + "scope": "resource" + }, + "javascript.suggest.names": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.names%", + "scope": "resource" + }, + "javascript.suggest.completeFunctionCalls": { + "type": "boolean", + "default": false, + "description": "%configuration.suggest.completeFunctionCalls%", + "scope": "resource" + }, + "typescript.suggest.completeFunctionCalls": { + "type": "boolean", + "default": false, + "description": "%configuration.suggest.completeFunctionCalls%", + "scope": "resource" + }, + "javascript.suggest.paths": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.paths%", + "scope": "resource" + }, + "typescript.suggest.paths": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.paths%", + "scope": "resource" + }, + "javascript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "scope": "language-overridable" + }, + "typescript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "scope": "language-overridable" + }, + "javascript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "scope": "language-overridable" + }, + "typescript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "scope": "language-overridable" + }, + "javascript.suggest.includeAutomaticOptionalChainCompletions": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "scope": "resource" + }, + "typescript.suggest.includeAutomaticOptionalChainCompletions": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "scope": "resource" + }, + "javascript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "scope": "resource" + }, + "typescript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "scope": "resource" + }, + "javascript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "scope": "resource" + }, + "typescript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "scope": "resource" + }, + "typescript.suggest.objectLiteralMethodSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.preferences%", + "order": 21, + "properties": { + "javascript.preferences.quoteStyle": { + "type": "string", + "enum": [ + "auto", + "single", + "double" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", + "markdownEnumDescriptions": [ + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" + ], + "scope": "language-overridable" + }, + "typescript.preferences.quoteStyle": { + "type": "string", + "enum": [ + "auto", + "single", + "double" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", + "markdownEnumDescriptions": [ + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" + ], + "scope": "language-overridable" + }, + "javascript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "scope": "language-overridable" + }, + "typescript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "scope": "language-overridable" + }, + "javascript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "language-overridable" + }, + "typescript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "language-overridable" + }, + "javascript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%javascript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "scope": "language-overridable" + }, + "typescript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "scope": "language-overridable" + }, + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.auto%", + "%typescript.preferences.includePackageJsonAutoImports.on%", + "%typescript.preferences.includePackageJsonAutoImports.off%" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", + "scope": "window" + }, + "javascript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "scope": "resource" + }, + "typescript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "scope": "resource" + }, + "javascript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "scope": "resource" + }, + "typescript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "scope": "resource" + }, + "typescript.preferences.preferTypeOnlyAutoImports": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", + "scope": "resource" + }, + "javascript.preferences.renameShorthandProperties": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", + "scope": "language-overridable" + }, + "typescript.preferences.renameShorthandProperties": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", + "scope": "language-overridable" + }, + "javascript.preferences.useAliasesForRenames": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "scope": "language-overridable" + }, + "typescript.preferences.useAliasesForRenames": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "scope": "language-overridable" + }, + "javascript.preferences.renameMatchingJsxTags": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.renameMatchingJsxTags%", + "scope": "language-overridable" + }, + "typescript.preferences.renameMatchingJsxTags": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.renameMatchingJsxTags%", + "scope": "language-overridable" + }, + "javascript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } + } + }, + "typescript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } } } - }, - "typescript.updateImportsOnFileMove.enabled": { - "type": "string", - "enum": [ - "prompt", - "always", - "never" - ], - "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" - ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "scope": "resource" - }, - "javascript.updateImportsOnFileMove.enabled": { - "type": "string", - "enum": [ - "prompt", - "always", - "never" - ], - "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" - ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "scope": "resource" - }, - "typescript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "scope": "language-overridable" - }, - "javascript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "scope": "language-overridable" - }, - "javascript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "scope": "language-overridable" - }, - "typescript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "scope": "language-overridable" - }, - "typescript.tsserver.useSeparateSyntaxServer": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.useSeparateSyntaxServer%", - "markdownDeprecationMessage": "%configuration.tsserver.useSeparateSyntaxServer.deprecation%", - "scope": "window" - }, - "typescript.tsserver.useSyntaxServer": { - "type": "string", - "scope": "window", - "description": "%configuration.tsserver.useSyntaxServer%", - "default": "auto", - "enum": [ - "always", - "never", - "auto" - ], - "enumDescriptions": [ - "%configuration.tsserver.useSyntaxServer.always%", - "%configuration.tsserver.useSyntaxServer.never%", - "%configuration.tsserver.useSyntaxServer.auto%" - ] - }, - "typescript.tsserver.maxTsServerMemory": { - "type": "number", - "default": 3072, - "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", - "scope": "window" - }, - "typescript.tsserver.experimental.enableProjectDiagnostics": { - "type": "boolean", - "default": false, - "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", - "scope": "window", - "tags": [ - "experimental" - ] - }, - "typescript.tsserver.experimental.useVsCodeWatcher": { - "type": "boolean", - "description": "%configuration.tsserver.useVsCodeWatcher%", - "deprecationMessage": "%configuration.tsserver.useVsCodeWatcher.deprecation%", - "default": true - }, - "typescript.tsserver.watchOptions": { - "description": "%configuration.tsserver.watchOptions%", - "scope": "window", - "default": "vscode", - "oneOf": [ - { - "type": "string", - "const": "vscode", - "description": "%configuration.tsserver.watchOptions.vscode%" - }, - { - "type": "object", - "properties": { - "watchFile": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.watchFile%", - "enum": [ - "fixedChunkSizePolling", - "fixedPollingInterval", - "priorityPollingInterval", - "dynamicPriorityPolling", - "useFsEvents", - "useFsEventsOnParentDirectory" - ], - "enumDescriptions": [ - "%configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling%", - "%configuration.tsserver.watchOptions.watchFile.fixedPollingInterval%", - "%configuration.tsserver.watchOptions.watchFile.priorityPollingInterval%", - "%configuration.tsserver.watchOptions.watchFile.dynamicPriorityPolling%", - "%configuration.tsserver.watchOptions.watchFile.useFsEvents%", - "%configuration.tsserver.watchOptions.watchFile.useFsEventsOnParentDirectory%" - ], - "default": "useFsEvents" - }, - "watchDirectory": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.watchDirectory%", - "enum": [ - "fixedChunkSizePolling", - "fixedPollingInterval", - "dynamicPriorityPolling", - "useFsEvents" - ], - "enumDescriptions": [ - "%configuration.tsserver.watchOptions.watchDirectory.fixedChunkSizePolling%", - "%configuration.tsserver.watchOptions.watchDirectory.fixedPollingInterval%", - "%configuration.tsserver.watchOptions.watchDirectory.dynamicPriorityPolling%", - "%configuration.tsserver.watchOptions.watchDirectory.useFsEvents%" - ], - "default": "useFsEvents" - }, - "fallbackPolling": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.fallbackPolling%", - "enum": [ - "fixedPollingInterval", - "priorityPollingInterval", - "dynamicPriorityPolling" - ], - "enumDescriptions": [ - "configuration.tsserver.watchOptions.fallbackPolling.fixedPollingInterval", - "configuration.tsserver.watchOptions.fallbackPolling.priorityPollingInterval", - "configuration.tsserver.watchOptions.fallbackPolling.dynamicPriorityPolling" - ] - }, - "synchronousWatchDirectory": { - "type": "boolean", - "description": "%configuration.tsserver.watchOptions.synchronousWatchDirectory%" + } + }, + { + "type": "object", + "title": "%configuration.format%", + "order": 23, + "properties": { + "javascript.format.enable": { + "type": "boolean", + "default": true, + "description": "%javascript.format.enable%", + "scope": "window" + }, + "typescript.format.enable": { + "type": "boolean", + "default": true, + "description": "%typescript.format.enable%", + "scope": "window" + }, + "javascript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "scope": "resource" + }, + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "scope": "resource" + }, + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "scope": "resource" + }, + "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "scope": "resource" + }, + "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterTypeAssertion%", + "scope": "resource" + }, + "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "scope": "resource" + }, + "typescript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "scope": "resource" + }, + "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "scope": "resource" + }, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "scope": "resource" + }, + "javascript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] + }, + "typescript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] + }, + "javascript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + }, + "typescript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.inlayHints%", + "order": 24, + "properties": { + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "resource" + }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "resource" + }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "resource" + }, + "javascript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "resource" + }, + "javascript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.server%", + "order": 25, + "properties": { + "typescript.tsserver.nodePath": { + "type": "string", + "description": "%configuration.tsserver.nodePath%", + "scope": "window" + }, + "typescript.npm": { + "type": "string", + "markdownDescription": "%typescript.npm%", + "scope": "machine" + }, + "typescript.check.npmIsInstalled": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.check.npmIsInstalled%", + "scope": "window" + }, + "typescript.tsserver.web.projectWideIntellisense.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.projectWideIntellisense.enabled%", + "scope": "window" + }, + "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", + "scope": "window" + }, + "typescript.tsserver.web.typeAcquisition.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.typeAcquisition.enabled%", + "scope": "window" + }, + "typescript.tsserver.useSeparateSyntaxServer": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.useSeparateSyntaxServer%", + "markdownDeprecationMessage": "%configuration.tsserver.useSeparateSyntaxServer.deprecation%", + "scope": "window" + }, + "typescript.tsserver.useSyntaxServer": { + "type": "string", + "scope": "window", + "description": "%configuration.tsserver.useSyntaxServer%", + "default": "auto", + "enum": [ + "always", + "never", + "auto" + ], + "enumDescriptions": [ + "%configuration.tsserver.useSyntaxServer.always%", + "%configuration.tsserver.useSyntaxServer.never%", + "%configuration.tsserver.useSyntaxServer.auto%" + ] + }, + "typescript.tsserver.maxTsServerMemory": { + "type": "number", + "default": 3072, + "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", + "scope": "window" + }, + "typescript.tsserver.experimental.enableProjectDiagnostics": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", + "scope": "window", + "tags": [ + "experimental" + ] + }, + "typescript.tsserver.experimental.useVsCodeWatcher": { + "type": "boolean", + "description": "%configuration.tsserver.useVsCodeWatcher%", + "deprecationMessage": "%configuration.tsserver.useVsCodeWatcher.deprecation%", + "default": true + }, + "typescript.tsserver.watchOptions": { + "description": "%configuration.tsserver.watchOptions%", + "scope": "window", + "default": "vscode", + "oneOf": [ + { + "type": "string", + "const": "vscode", + "description": "%configuration.tsserver.watchOptions.vscode%" + }, + { + "type": "object", + "properties": { + "watchFile": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.watchFile%", + "enum": [ + "fixedChunkSizePolling", + "fixedPollingInterval", + "priorityPollingInterval", + "dynamicPriorityPolling", + "useFsEvents", + "useFsEventsOnParentDirectory" + ], + "enumDescriptions": [ + "%configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling%", + "%configuration.tsserver.watchOptions.watchFile.fixedPollingInterval%", + "%configuration.tsserver.watchOptions.watchFile.priorityPollingInterval%", + "%configuration.tsserver.watchOptions.watchFile.dynamicPriorityPolling%", + "%configuration.tsserver.watchOptions.watchFile.useFsEvents%", + "%configuration.tsserver.watchOptions.watchFile.useFsEventsOnParentDirectory%" + ], + "default": "useFsEvents" + }, + "watchDirectory": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.watchDirectory%", + "enum": [ + "fixedChunkSizePolling", + "fixedPollingInterval", + "dynamicPriorityPolling", + "useFsEvents" + ], + "enumDescriptions": [ + "%configuration.tsserver.watchOptions.watchDirectory.fixedChunkSizePolling%", + "%configuration.tsserver.watchOptions.watchDirectory.fixedPollingInterval%", + "%configuration.tsserver.watchOptions.watchDirectory.dynamicPriorityPolling%", + "%configuration.tsserver.watchOptions.watchDirectory.useFsEvents%" + ], + "default": "useFsEvents" + }, + "fallbackPolling": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.fallbackPolling%", + "enum": [ + "fixedPollingInterval", + "priorityPollingInterval", + "dynamicPriorityPolling" + ], + "enumDescriptions": [ + "configuration.tsserver.watchOptions.fallbackPolling.fixedPollingInterval", + "configuration.tsserver.watchOptions.fallbackPolling.priorityPollingInterval", + "configuration.tsserver.watchOptions.fallbackPolling.dynamicPriorityPolling" + ] + }, + "synchronousWatchDirectory": { + "type": "boolean", + "description": "%configuration.tsserver.watchOptions.synchronousWatchDirectory%" + } } } - } - ] - }, - "typescript.workspaceSymbols.scope": { - "type": "string", - "enum": [ - "allOpenProjects", - "currentProject" - ], - "enumDescriptions": [ - "%typescript.workspaceSymbols.scope.allOpenProjects%", - "%typescript.workspaceSymbols.scope.currentProject%" - ], - "default": "allOpenProjects", - "markdownDescription": "%typescript.workspaceSymbols.scope%", - "scope": "window" - }, - "javascript.suggest.classMemberSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "scope": "resource" - }, - "typescript.suggest.classMemberSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "scope": "resource" - }, - "typescript.suggest.objectLiteralMethodSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", - "scope": "resource" - }, - "typescript.tsserver.web.projectWideIntellisense.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.web.projectWideIntellisense.enabled%", - "scope": "window" - }, - "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { - "type": "boolean", - "default": false, - "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", - "scope": "window" - }, - "typescript.tsserver.web.typeAcquisition.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.web.typeAcquisition.enabled%", - "scope": "window" - }, - "typescript.tsserver.nodePath": { - "type": "string", - "description": "%configuration.tsserver.nodePath%", - "scope": "window" - }, - "typescript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "scope": "window" - }, - "javascript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "scope": "window" - }, - "typescript.workspaceSymbols.excludeLibrarySymbols": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", - "scope": "window" - }, - "typescript.tsserver.enableRegionDiagnostics": { - "type": "boolean", - "default": true, - "description": "%typescript.tsserver.enableRegionDiagnostics%", - "scope": "window" - }, - "javascript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%" - }, - "typescript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%" + ] + }, + "typescript.tsserver.enableTracing": { + "type": "boolean", + "default": false, + "description": "%typescript.tsserver.enableTracing%", + "scope": "window" + }, + "typescript.tsserver.log": { + "type": "string", + "enum": [ + "off", + "terse", + "normal", + "verbose" + ], + "default": "off", + "description": "%typescript.tsserver.log%", + "scope": "window" + }, + "typescript.tsserver.pluginPaths": { + "type": "array", + "items": { + "type": "string", + "description": "%typescript.tsserver.pluginPaths.item%" + }, + "default": [], + "description": "%typescript.tsserver.pluginPaths%", + "scope": "machine" + } } } - }, + ], "commands": [ { "command": "typescript.reloadProjects", @@ -1576,6 +1631,18 @@ "command": "javascript.removeUnusedImports", "title": "%typescript.removeUnusedImports%", "category": "JavaScript" + }, + { + "command": "typescript.experimental.enableTsgo", + "title": "Use TypeScript Go (Experimental)", + "category": "TypeScript", + "enablement": "!config.typescript.experimental.useTsgo && config.typescript-go.executablePath" + }, + { + "command": "typescript.experimental.disableTsgo", + "title": "Stop using TypeScript Go (Experimental)", + "category": "TypeScript", + "enablement": "config.typescript.experimental.useTsgo" } ], "menus": { diff --git a/code/extensions/typescript-language-features/package.nls.json b/code/extensions/typescript-language-features/package.nls.json index 447359e11a5..44fa54efbcd 100644 --- a/code/extensions/typescript-language-features/package.nls.json +++ b/code/extensions/typescript-language-features/package.nls.json @@ -5,9 +5,15 @@ "virtualWorkspaces": "In virtual workspaces, resolving and finding references across files is not supported.", "reloadProjects.title": "Reload Project", "configuration.typescript": "TypeScript", + "configuration.preferences": "Preferences", + "configuration.format": "Formatting", + "configuration.suggest": "Suggestions", + "configuration.inlayHints": "Inlay Hints", + "configuration.server": "TS Server", "configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.", "configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.", "configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.", + "typescript.useTsgo": "Disables TypeScript and JavaScript language features to allow usage of the TypeScript Go experimental extension. Requires TypeScript Go to be installed and configured. Requires reloading extensions after changing this setting.", "typescript.tsdk.desc": "Specifies the folder path to the tsserver and `lib*.d.ts` files under a TypeScript install to use for IntelliSense, for example: `./node_modules/typescript/lib`.\n\n- When specified as a user setting, the TypeScript version from `typescript.tsdk` automatically replaces the built-in TypeScript version.\n- When specified as a workspace setting, `typescript.tsdk` allows you to switch to use that workspace version of TypeScript for IntelliSense with the `TypeScript: Select TypeScript version` command.\n\nSee the [TypeScript documentation](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions) for more detail about managing TypeScript versions.", "typescript.disableAutomaticTypeAcquisition": "Disables [automatic type acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition). Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.", "typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.", @@ -163,7 +169,7 @@ "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", "typescript.updateImportsOnFileMove.enabled.never": "Never rename paths and don't prompt.", "typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags.", - "typescript.suggest.enabled": "Enabled/disable autocomplete suggestions.", + "typescript.suggest.enabled": "Enable/disable autocomplete suggestions.", "configuration.suggest.completeJSDocs": "Enable/disable suggestion to complete JSDoc comments.", "configuration.tsserver.useVsCodeWatcher": "Use VS Code's file watchers instead of TypeScript's. Requires using TypeScript 5.4+ in the workspace.", "configuration.tsserver.useVsCodeWatcher.deprecation": "Please use the `#typescript.tsserver.watchOptions#` setting instead.", @@ -224,6 +230,7 @@ "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", "configuration.updateImportsOnPaste": "Automatically update imports when pasting code. Requires TypeScript 5.6+.", + "configuration.expandableHover": "Enable expanding/contracting the hover to reveal more/less information from the TS server. Requires TypeScript 5.9+.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", diff --git a/code/extensions/typescript-language-features/src/commands/index.ts b/code/extensions/typescript-language-features/src/commands/index.ts index 7130f201975..6131d9b985f 100644 --- a/code/extensions/typescript-language-features/src/commands/index.ts +++ b/code/extensions/typescript-language-features/src/commands/index.ts @@ -9,6 +9,7 @@ import { ActiveJsTsEditorTracker } from '../ui/activeJsTsEditorTracker'; import { Lazy } from '../utils/lazy'; import { CommandManager } from './commandManager'; import { ConfigurePluginCommand } from './configurePlugin'; +import { EnableTsgoCommand, DisableTsgoCommand } from './useTsgo'; import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration'; import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings'; import { OpenJsDocLinkCommand } from './openJsDocLink'; @@ -35,4 +36,6 @@ export function registerBaseCommands( commandManager.register(new LearnMoreAboutRefactoringsCommand()); commandManager.register(new TSServerRequestCommand(lazyClientHost)); commandManager.register(new OpenJsDocLinkCommand()); + commandManager.register(new EnableTsgoCommand()); + commandManager.register(new DisableTsgoCommand()); } diff --git a/code/extensions/typescript-language-features/src/commands/tsserverRequests.ts b/code/extensions/typescript-language-features/src/commands/tsserverRequests.ts index 0fc70b33bf5..7c925024b96 100644 --- a/code/extensions/typescript-language-features/src/commands/tsserverRequests.ts +++ b/code/extensions/typescript-language-features/src/commands/tsserverRequests.ts @@ -16,6 +16,7 @@ function isCancellationToken(value: any): value is vscode.CancellationToken { interface RequestArgs { readonly file?: unknown; + readonly $traceId?: unknown; } export class TSServerRequestCommand implements Command { @@ -31,11 +32,18 @@ export class TSServerRequestCommand implements Command { } if (args && typeof args === 'object' && !Array.isArray(args)) { const requestArgs = args as RequestArgs; - let newArgs: any = undefined; - if (requestArgs.file instanceof vscode.Uri) { - newArgs = { ...args }; - const client = this.lazyClientHost.value.serviceClient; - newArgs.file = client.toOpenTsFilePath(requestArgs.file); + const hasFile = requestArgs.file instanceof vscode.Uri; + const hasTraceId = typeof requestArgs.$traceId === 'string'; + if (hasFile || hasTraceId) { + const newArgs = { ...args }; + if (hasFile) { + const client = this.lazyClientHost.value.serviceClient; + newArgs.file = client.toOpenTsFilePath(requestArgs.file); + } + if (hasTraceId) { + const telemetryReporter = this.lazyClientHost.value.serviceClient.telemetryReporter; + telemetryReporter.logTraceEvent('TSServerRequestCommand.execute', requestArgs.$traceId, JSON.stringify({ command })); + } args = newArgs; } } diff --git a/code/extensions/typescript-language-features/src/commands/useTsgo.ts b/code/extensions/typescript-language-features/src/commands/useTsgo.ts new file mode 100644 index 00000000000..aedf28e54b0 --- /dev/null +++ b/code/extensions/typescript-language-features/src/commands/useTsgo.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Command } from './commandManager'; + +export class EnableTsgoCommand implements Command { + public readonly id = 'typescript.experimental.enableTsgo'; + + public async execute(): Promise { + await updateTsgoSetting(true); + } +} + +export class DisableTsgoCommand implements Command { + public readonly id = 'typescript.experimental.disableTsgo'; + + public async execute(): Promise { + await updateTsgoSetting(false); + } +} + +/** + * Updates the TypeScript Go setting and reloads extension host. + * @param enable Whether to enable or disable TypeScript Go + */ +async function updateTsgoSetting(enable: boolean): Promise { + const tsgoExtension = vscode.extensions.getExtension('typescript.typescript-lsp'); + // Error if the TypeScript Go extension is not installed with a button to open the GitHub repo + if (!tsgoExtension) { + const selection = await vscode.window.showErrorMessage( + vscode.l10n.t('The TypeScript Go extension is not installed.'), + { + title: vscode.l10n.t('Open on GitHub'), + isCloseAffordance: true, + } + ); + + if (selection) { + await vscode.env.openExternal(vscode.Uri.parse('https://github.com/microsoft/typescript-go')); + } + } + + const tsConfig = vscode.workspace.getConfiguration('typescript'); + const currentValue = tsConfig.get('experimental.useTsgo', false); + if (currentValue === enable) { + return; + } + + // Determine the target scope for the configuration update + let target = vscode.ConfigurationTarget.Global; + const inspect = tsConfig.inspect('experimental.useTsgo'); + if (inspect?.workspaceValue !== undefined) { + target = vscode.ConfigurationTarget.Workspace; + } else if (inspect?.workspaceFolderValue !== undefined) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + } else { + // If setting is not defined yet, use the same scope as typescript-go.executablePath + const tsgoConfig = vscode.workspace.getConfiguration('typescript-go'); + const tsgoInspect = tsgoConfig.inspect('executablePath'); + + if (tsgoInspect?.workspaceValue !== undefined) { + target = vscode.ConfigurationTarget.Workspace; + } else if (tsgoInspect?.workspaceFolderValue !== undefined) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + } + } + + // Update the setting, restart the extension host, and enable the TypeScript Go extension + await tsConfig.update('experimental.useTsgo', enable, target); + await vscode.commands.executeCommand('workbench.action.restartExtensionHost'); +} diff --git a/code/extensions/typescript-language-features/src/extension.browser.ts b/code/extensions/typescript-language-features/src/extension.browser.ts index b87a41901bb..f39740bab23 100644 --- a/code/extensions/typescript-language-features/src/extension.browser.ts +++ b/code/extensions/typescript-language-features/src/extension.browser.ts @@ -61,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.6.2'))); + API.fromSimpleString('5.8.3'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); diff --git a/code/extensions/typescript-language-features/src/extension.ts b/code/extensions/typescript-language-features/src/extension.ts index 83bd22e3eca..29f809bd653 100644 --- a/code/extensions/typescript-language-features/src/extension.ts +++ b/code/extensions/typescript-language-features/src/extension.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; import { Api, getExtensionApi } from './api'; import { CommandManager } from './commands/commandManager'; +import { DisableTsgoCommand } from './commands/useTsgo'; import { registerBaseCommands } from './commands/index'; import { ElectronServiceConfigurationProvider } from './configuration/configuration.electron'; import { ExperimentationTelemetryReporter, IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; @@ -28,12 +29,26 @@ import * as temp from './utils/temp.electron'; export function activate( context: vscode.ExtensionContext ): Api { - const pluginManager = new PluginManager(); - context.subscriptions.push(pluginManager); - const commandManager = new CommandManager(); context.subscriptions.push(commandManager); + // Disable extension if using the experimental TypeScript Go extension + const config = vscode.workspace.getConfiguration('typescript'); + const useTsgo = config.get('experimental.useTsgo', false); + + if (useTsgo) { + commandManager.register(new DisableTsgoCommand()); + // Return a no-op API when disabled + return { + getAPI() { + return undefined; + } + }; + } + + const pluginManager = new PluginManager(); + context.subscriptions.push(pluginManager); + const onCompletionAccepted = new vscode.EventEmitter(); context.subscriptions.push(onCompletionAccepted); diff --git a/code/extensions/typescript-language-features/src/languageFeatures/hover.ts b/code/extensions/typescript-language-features/src/languageFeatures/hover.ts index 3012658036f..ff7719c0986 100644 --- a/code/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/code/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -11,10 +11,11 @@ import { DocumentSelector } from '../configuration/documentSelector'; import { documentationToMarkdown } from './util/textRendering'; import * as typeConverters from '../typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; - +import { API } from '../tsServer/api'; class TypeScriptHoverProvider implements vscode.HoverProvider { + private lastHoverAndLevel: [vscode.Hover, number] | undefined; public constructor( private readonly client: ITypeScriptServiceClient, @@ -24,17 +25,24 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { public async provideHover( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken - ): Promise { + token: vscode.CancellationToken, + context?: vscode.HoverContext, + ): Promise { const filepath = this.client.toOpenTsFilePath(document); if (!filepath) { return undefined; } + const enableExpandableHover = vscode.workspace.getConfiguration('typescript').get('experimental.expandableHover', true); + let verbosityLevel: number | undefined; + if (enableExpandableHover && this.client.apiVersion.gte(API.v590)) { + verbosityLevel = Math.max(0, this.getPreviousLevel(context?.previousHover) + (context?.verbosityDelta ?? 0)); + } + const args = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, position), verbosityLevel }; + const response = await this.client.interruptGetErr(async () => { await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); - const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position); return this.client.execute('quickinfo', args, token); }); @@ -42,9 +50,24 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { return undefined; } - return new vscode.Hover( - this.getContents(document.uri, response.body, response._serverType), - typeConverters.Range.fromTextSpan(response.body)); + const contents = this.getContents(document.uri, response.body, response._serverType); + const range = typeConverters.Range.fromTextSpan(response.body); + const hover = verbosityLevel !== undefined ? + new vscode.VerboseHover( + contents, + range, + // @ts-expect-error + /*canIncreaseVerbosity*/ response.body.canIncreaseVerbosityLevel, + /*canDecreaseVerbosity*/ verbosityLevel !== 0 + ) : new vscode.Hover( + contents, + range + ); + + if (verbosityLevel !== undefined) { + this.lastHoverAndLevel = [hover, verbosityLevel]; + } + return hover; } private getContents( @@ -72,6 +95,13 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { parts.push(md); return parts; } + + private getPreviousLevel(previousHover: vscode.Hover | undefined): number { + if (previousHover && this.lastHoverAndLevel && this.lastHoverAndLevel[0] === previousHover) { + return this.lastHoverAndLevel[1]; + } + return 0; + } } export function register( diff --git a/code/extensions/typescript-language-features/src/logging/telemetry.ts b/code/extensions/typescript-language-features/src/logging/telemetry.ts index 4b2d3867f6d..b96487b2419 100644 --- a/code/extensions/typescript-language-features/src/logging/telemetry.ts +++ b/code/extensions/typescript-language-features/src/logging/telemetry.ts @@ -11,6 +11,7 @@ export interface TelemetryProperties { export interface TelemetryReporter { logTelemetry(eventName: string, properties?: TelemetryProperties): void; + logTraceEvent(tracePoint: string, correlationId: string, command?: string): void; } export class VSCodeTelemetryReporter implements TelemetryReporter { @@ -34,4 +35,27 @@ export class VSCodeTelemetryReporter implements TelemetryReporter { reporter.postEventObj(eventName, properties); } + + public logTraceEvent(point: string, id: string, data?: string): void { + const event: { point: string; id: string; data?: string | undefined } = { + point, + id + }; + if (data) { + event.data = data; + } + + /* __GDPR__ + "typeScriptExtension.trace" : { + "owner": "dirkb", + "${include}": [ + "${TypeScriptCommonProperties}" + ], + "point" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The trace point." }, + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The traceId is used to correlate the request with other trace points." }, + "data": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Additional data" } + } + */ + this.logTelemetry('typeScriptExtension.trace', event); + } } diff --git a/code/extensions/typescript-language-features/src/test/unit/server.test.ts b/code/extensions/typescript-language-features/src/test/unit/server.test.ts index 4d086be5b88..ae4a05a6555 100644 --- a/code/extensions/typescript-language-features/src/test/unit/server.test.ts +++ b/code/extensions/typescript-language-features/src/test/unit/server.test.ts @@ -18,6 +18,7 @@ import { nulToken } from '../../utils/cancellation'; const NoopTelemetryReporter = new class implements TelemetryReporter { logTelemetry(): void { /* noop */ } + logTraceEvent(): void { /* noop */ } dispose(): void { /* noop */ } }; diff --git a/code/extensions/typescript-language-features/src/tsServer/api.ts b/code/extensions/typescript-language-features/src/tsServer/api.ts index 4ddc29944f0..ef3ce4c933d 100644 --- a/code/extensions/typescript-language-features/src/tsServer/api.ts +++ b/code/extensions/typescript-language-features/src/tsServer/api.ts @@ -30,6 +30,7 @@ export class API { public static readonly v540 = API.fromSimpleString('5.4.0'); public static readonly v560 = API.fromSimpleString('5.6.0'); public static readonly v570 = API.fromSimpleString('5.7.0'); + public static readonly v590 = API.fromSimpleString('5.9.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/code/extensions/typescript-language-features/src/tsServer/callbackMap.ts b/code/extensions/typescript-language-features/src/tsServer/callbackMap.ts index 57a80051e6d..1484b0ff654 100644 --- a/code/extensions/typescript-language-features/src/tsServer/callbackMap.ts +++ b/code/extensions/typescript-language-features/src/tsServer/callbackMap.ts @@ -11,6 +11,7 @@ export interface CallbackItem { readonly onError: (err: Error) => void; readonly queuingStartTime: number; readonly isAsync: boolean; + readonly traceId?: string | undefined; } export class CallbackMap { @@ -43,6 +44,10 @@ export class CallbackMap { return callback; } + public peek(seq: number): CallbackItem | undefined> | undefined { + return this._callbacks.get(seq) ?? this._asyncCallbacks.get(seq); + } + private delete(seq: number) { if (!this._callbacks.delete(seq)) { this._asyncCallbacks.delete(seq); diff --git a/code/extensions/typescript-language-features/src/tsServer/server.ts b/code/extensions/typescript-language-features/src/tsServer/server.ts index 4e41f7aa79a..dbb867f8bb3 100644 --- a/code/extensions/typescript-language-features/src/tsServer/server.ts +++ b/code/extensions/typescript-language-features/src/tsServer/server.ts @@ -185,6 +185,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { private tryCancelRequest(request: Proto.Request, command: string): boolean { const seq = request.seq; + const callback = this._callbacks.peek(seq); + if (callback?.traceId !== undefined) { + this._telemetryReporter.logTraceEvent('TSServer.tryCancelRequest', callback.traceId, JSON.stringify({ command, cancelled: true })); + } try { if (this._requestQueue.tryDeletePendingRequest(seq)) { this.logTrace(`Canceled request with sequence number ${seq}`); @@ -206,7 +210,9 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { if (!callback) { return; } - + if (callback.traceId !== undefined) { + this._telemetryReporter.logTraceEvent('TSServerRequest.dispatchResponse', callback.traceId, JSON.stringify({ command: response.command, success: response.success, performanceData: response.performanceData })); + } this._tracer.traceResponse(this._serverId, response, callback); if (response.success) { callback.onSuccess(response); @@ -218,7 +224,7 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { } } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean; token?: vscode.CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget }): Array> | undefined> { + public executeImpl(command: keyof TypeScriptRequests, args: unknown, executeInfo: { isAsync: boolean; token?: vscode.CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget }): Array> | undefined> { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -229,7 +235,7 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { let result: Promise> | undefined; if (executeInfo.expectsResult) { result = new Promise>((resolve, reject) => { - this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync); + this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync, traceId: request.arguments?.$traceId }, executeInfo.isAsync); if (executeInfo.token) { @@ -263,6 +269,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { } this._requestQueue.enqueue(requestInfo); + if (args && typeof (args as any).$traceId === 'string') { + const queueLength = this._requestQueue.length - 1; + this._telemetryReporter.logTraceEvent('TSServer.enqueueRequest', (args as any).$traceId, JSON.stringify({ command, queueLength })); + } this.sendNextRequests(); return [result]; @@ -287,6 +297,9 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { try { this.write(serverRequest); + if (typeof serverRequest.arguments?.$traceId === 'string') { + this._telemetryReporter.logTraceEvent('TSServer.sendRequest', serverRequest.arguments.$traceId, JSON.stringify({ command: serverRequest.command })); + } } catch (err) { const callback = this.fetchCallback(serverRequest.seq); callback?.onError(err); diff --git a/code/extensions/typescript-language-features/src/tsconfig.ts b/code/extensions/typescript-language-features/src/tsconfig.ts index e85c715e875..01a88a43f34 100644 --- a/code/extensions/typescript-language-features/src/tsconfig.ts +++ b/code/extensions/typescript-language-features/src/tsconfig.ts @@ -29,7 +29,7 @@ export function inferredProjectCompilerOptions( module: (version.gte(API.v540) ? 'Preserve' : 'ESNext') as Proto.ModuleKind, moduleResolution: (version.gte(API.v540) ? 'Bundler' : 'Node') as Proto.ModuleResolutionKind, target: 'ES2022' as Proto.ScriptTarget, - jsx: 'react' as Proto.JsxEmit, + jsx: 'react-jsx' as Proto.JsxEmit, }; if (version.gte(API.v500)) { diff --git a/code/extensions/typescript-language-features/tsconfig.json b/code/extensions/typescript-language-features/tsconfig.json index f604952abd2..776a71efaf8 100644 --- a/code/extensions/typescript-language-features/tsconfig.json +++ b/code/extensions/typescript-language-features/tsconfig.json @@ -14,6 +14,7 @@ "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts" + "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", + "../../src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts", ] } diff --git a/code/extensions/vscode-api-tests/package-lock.json b/code/extensions/vscode-api-tests/package-lock.json index cd90b33ca49..9a80cf4f19b 100644 --- a/code/extensions/vscode-api-tests/package-lock.json +++ b/code/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/code/extensions/vscode-api-tests/package.json b/code/extensions/vscode-api-tests/package.json index 66a80b8867f..0d7f87a9be4 100644 --- a/code/extensions/vscode-api-tests/package.json +++ b/code/extensions/vscode-api-tests/package.json @@ -44,7 +44,6 @@ "terminalDataWriteEvent", "terminalDimensions", "testObserver", - "textDocumentEncoding", "textSearchProvider", "timeline", "tokenInformation", diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 289f0a6a1c0..df90a035401 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { ChatContext, ChatRequest, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; +import { ChatContext, ChatRequest, ChatRequestTurn, ChatRequestTurn2, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils'; suite('chat', () => { @@ -71,6 +71,7 @@ suite('chat', () => { assert.strictEqual(request.context.history.length, 2); assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].command, 'hello'); + assert.ok(request.context.history[0] instanceof ChatRequestTurn && request.context.history[0] instanceof ChatRequestTurn2); deferred.complete(); } } catch (e) { diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts index 69810825b1b..55a17cd3ce0 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -73,7 +73,7 @@ async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) { await saveAllFilesAndCloseAll(); }); - test('Can open an interactive window and execute from input box', async () => { + test.skip('Can open an interactive window and execute from input box', async () => { assert.ok(vscode.workspace.workspaceFolders); const { notebookEditor, inputUri } = await createInteractiveWindow(defaultKernel); diff --git a/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index ceb3aea00d5..7ee205256bb 100644 --- a/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/code/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1373,13 +1373,20 @@ suite('vscode API - workspace', () => { let err; try { - await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), doc1.uri); + await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), { uri: doc1.uri }); } catch (e) { err = e; } assert.ok(err); }); + test('encoding: openTextDocument - invalid encoding falls back to default', async () => { + const uri1 = await createRandomFile(); + + const doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'foobar123' }); + assert.strictEqual(doc1.encoding, 'utf8'); + }); + test('encoding: openTextDocument - multiple requests with different encoding work', async () => { const uri1 = await createRandomFile(); @@ -1396,18 +1403,18 @@ suite('vscode API - workspace', () => { const uri = root.with({ path: posix.join(root.path, 'file.txt') }); // without setting - assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hello World'), uri), 'Hello World'); - assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hellö Wörld'), uri), 'Hellö Wörld'); - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]), uri), 'Hello World'); // UTF-8 with BOM - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]), uri), 'Hello World'); // UTF-16 BE with BOM - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]), uri), 'Hello World'); // UTF-16 LE with BOM - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]), uri), 'Hello World'); - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]), uri), 'Hello World'); + assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hello World'), { uri }), 'Hello World'); + assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hellö Wörld'), { uri }), 'Hellö Wörld'); + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]), { uri }), 'Hello World'); // UTF-8 with BOM + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]), { uri }), 'Hello World'); // UTF-16 BE with BOM + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]), { uri }), 'Hello World'); // UTF-16 LE with BOM + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]), { uri }), 'Hello World'); + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]), { uri }), 'Hello World'); // with auto-guess encoding try { await vscode.workspace.getConfiguration('files', uri).update('autoGuessEncoding', true, vscode.ConfigurationTarget.Global); - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), uri), 'Hellö Wörld'); + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), { uri }), 'Hellö Wörld'); } finally { await vscode.workspace.getConfiguration('files', uri).update('autoGuessEncoding', false, vscode.ConfigurationTarget.Global); } @@ -1415,19 +1422,19 @@ suite('vscode API - workspace', () => { // with encoding setting try { await vscode.workspace.getConfiguration('files', uri).update('encoding', 'windows1252', vscode.ConfigurationTarget.Global); - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), uri), 'Hellö Wörld'); + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), { uri }), 'Hellö Wörld'); } finally { await vscode.workspace.getConfiguration('files', uri).update('encoding', 'utf8', vscode.ConfigurationTarget.Global); } // with encoding provided - assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), uri, { encoding: 'windows1252' }), 'Hellö Wörld'); - assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hello World'), uri, { encoding: 'foobar123' }), 'Hello World'); + assert.strictEqual(await vscode.workspace.decode(new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]), { encoding: 'windows1252' }), 'Hellö Wörld'); + assert.strictEqual(await vscode.workspace.decode(Buffer.from('Hello World'), { encoding: 'foobar123' }), 'Hello World'); // binary let err; try { - await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), uri); + await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), { uri }); } catch (e) { err = e; } @@ -1438,32 +1445,32 @@ suite('vscode API - workspace', () => { const uri = root.with({ path: posix.join(root.path, 'file.txt') }); // without setting - assert.strictEqual((await vscode.workspace.encode('Hello World', uri)).toString(), 'Hello World'); + assert.strictEqual((await vscode.workspace.encode('Hello World', { uri })).toString(), 'Hello World'); // with encoding setting try { await vscode.workspace.getConfiguration('files', uri).update('encoding', 'utf8bom', vscode.ConfigurationTarget.Global); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri), new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { uri }), new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); await vscode.workspace.getConfiguration('files', uri).update('encoding', 'utf16le', vscode.ConfigurationTarget.Global); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri), new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { uri }), new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]))); await vscode.workspace.getConfiguration('files', uri).update('encoding', 'utf16be', vscode.ConfigurationTarget.Global); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri), new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { uri }), new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]))); await vscode.workspace.getConfiguration('files', uri).update('encoding', 'cp1252', vscode.ConfigurationTarget.Global); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hellö Wörld', uri), new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hellö Wörld', { uri }), new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]))); } finally { await vscode.workspace.getConfiguration('files', uri).update('encoding', 'utf8', vscode.ConfigurationTarget.Global); } // with encoding provided - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri, { encoding: 'utf8' }), new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri, { encoding: 'utf8bom' }), new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri, { encoding: 'utf16le' }), new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]))); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri, { encoding: 'utf16be' }), new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]))); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hellö Wörld', uri, { encoding: 'cp1252' }), new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]))); - assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', uri, { encoding: 'foobar123' }), new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { encoding: 'utf8' }), new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { encoding: 'utf8bom' }), new Uint8Array([0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { encoding: 'utf16le' }), new Uint8Array([0xFF, 0xFE, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { encoding: 'utf16be' }), new Uint8Array([0xFE, 0xFF, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hellö Wörld', { encoding: 'cp1252' }), new Uint8Array([72, 101, 108, 108, 0xF6, 32, 87, 0xF6, 114, 108, 100]))); + assert.ok(equalsUint8Array(await vscode.workspace.encode('Hello World', { encoding: 'foobar123' }), new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))); }); function equalsUint8Array(a: Uint8Array, b: Uint8Array): boolean { @@ -1490,7 +1497,7 @@ suite('vscode API - workspace', () => { const text = doc.getText(); assert.strictEqual(text, originalText); - const buf = await vscode.workspace.encode(text, uri, { encoding: 'windows1252' }); + const buf = await vscode.workspace.encode(text, { encoding: 'windows1252' }); await vscode.workspace.fs.writeFile(uri, buf); doc = await vscode.workspace.openTextDocument(uri, { encoding: 'windows1252' }); @@ -1509,10 +1516,10 @@ suite('vscode API - workspace', () => { doc = await vscode.workspace.openTextDocument(uri, { encoding: 'utf8bom' }); assert.strictEqual(doc.encoding, 'utf8bom'); - const decoded = await vscode.workspace.decode(new Uint8Array(buffer), uri, { encoding: 'utf8bom' }); + const decoded = await vscode.workspace.decode(new Uint8Array(buffer), { encoding: 'utf8bom' }); assert.strictEqual(decoded, 'Hello World'); - const encoded = await vscode.workspace.encode('Hello World', uri, { encoding: 'utf8bom' }); + const encoded = await vscode.workspace.encode('Hello World', { encoding: 'utf8bom' }); assert.ok(equalsUint8Array(encoded, new Uint8Array(buffer))); }); }); diff --git a/code/extensions/vscode-api-tests/src/utils.ts b/code/extensions/vscode-api-tests/src/utils.ts index 7223fa8a6a1..4bf02420715 100644 --- a/code/extensions/vscode-api-tests/src/utils.ts +++ b/code/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/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts index ff95456b856..ebfddb5daa1 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts +++ b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts @@ -45,3 +45,6 @@ function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { type StringNumberBooleans = [string, number, ...boolean[]]; type StringBooleansNumber = [string, ...boolean[], number]; type BooleansStringNumber = [...boolean[], string, number]; + +let s = '2'; ++s; diff --git a/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.css b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.css index f9151198c5e..88b01799a1e 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.css +++ b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.css @@ -16,7 +16,7 @@ } body { font: 75% georgia, sans-serif; - line-height: 1.88889; + line-height: 1.88889 !important; color: #555753; background: #fff url(blossoms.jpg) no-repeat bottom right; margin: 0; @@ -63,6 +63,11 @@ abbr { border-bottom: none; } +@property --gradient-angle { + syntax: ''; + initial-value: 0deg; + inherits: false; +} /* specific divs */ @@ -73,7 +78,7 @@ abbr { position: relative; } -.intro { +.intro span::after{ min-width: 470px; width: 100%; } @@ -149,6 +154,20 @@ footer a:visited { color: '#B3AE94'; } +.parent { + color: tomato; + .child { + color: blue; + } +} + +.parent { + color: tomato; + & .child { + color: blue; + } +} + .extra1 { background: transparent url(cr2.gif) top left no-repeat; position: absolute; @@ -161,3 +180,21 @@ footer a:visited { .chat-feature-container .codicon[class*='codicon-'] { font-size: 16px; } + +figma-help-bubble { + position: absolute; + right: 16px; + bottom: 16px; +} + +figma-select::part(listbox) { + max-height: 250px; +} + +div > * + * { + margin-top: 4rem; +} + +* { + box-sizing: border-box; +} diff --git a/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.regexp.ts b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.regexp.ts index 115c0d8a9a0..734dac8d9b2 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.regexp.ts +++ b/code/extensions/vscode-colorize-tests/test/colorize-fixtures/test.regexp.ts @@ -4,3 +4,4 @@ const c = /\r\n|\r|\n/; const d = /\/\/# sourceMappingURL=[^ ]+$/; const e = /<%=\s*([^\s]+)\s*%>/; const f = /```suggestion(\u0020*(\r\n|\n))((?[\s\S]*?)(\r\n|\n))?```/; +const g = /(?<=^|\s)(?=[a-z])([a-z])(?=.*\1$)\(([^()]*0+)(?", + "t": "source.css meta.at-rule.body.css meta.selector.css keyword.operator.combinator.css", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "';", + "t": "source.css meta.at-rule.body.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.css meta.at-rule.body.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "initial-value", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.custom.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": ": 0deg;", + "t": "source.css meta.at-rule.body.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\tinherits: false;", + "t": "source.css meta.at-rule.body.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.at-rule.body.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, { "c": "/*", - "t": "source.css comment.block.css punctuation.definition.comment.begin.css", + "t": "source.css meta.at-rule.body.css comment.block.css punctuation.definition.comment.begin.css", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -4901,7 +5125,7 @@ }, { "c": " specific divs ", - "t": "source.css comment.block.css", + "t": "source.css meta.at-rule.body.css comment.block.css", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -4915,7 +5139,7 @@ }, { "c": "*/", - "t": "source.css comment.block.css punctuation.definition.comment.end.css", + "t": "source.css meta.at-rule.body.css comment.block.css punctuation.definition.comment.end.css", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -4929,7 +5153,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -4943,7 +5167,7 @@ }, { "c": "page-wrapper", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -4957,7 +5181,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4971,7 +5195,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4985,7 +5209,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4999,7 +5223,7 @@ }, { "c": "background", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5013,7 +5237,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5027,7 +5251,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5041,7 +5265,7 @@ }, { "c": "url", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", "r": { "dark_plus": "support.function: #DCDCAA", "light_plus": "support.function: #795E26", @@ -5055,7 +5279,7 @@ }, { "c": "(", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5069,7 +5293,7 @@ }, { "c": "zen-bg.jpg", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", "r": { "dark_plus": "source.css variable: #9CDCFE", "light_plus": "source.css variable: #E50000", @@ -5083,7 +5307,7 @@ }, { "c": ")", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5097,7 +5321,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5111,7 +5335,7 @@ }, { "c": "no-repeat", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -5125,7 +5349,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5139,7 +5363,7 @@ }, { "c": "top", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -5153,7 +5377,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5167,7 +5391,7 @@ }, { "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -5181,7 +5405,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5195,7 +5419,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5209,7 +5433,7 @@ }, { "c": "padding", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5223,7 +5447,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5237,7 +5461,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5251,7 +5475,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5265,7 +5489,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5279,7 +5503,7 @@ }, { "c": "175", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5293,7 +5517,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -5307,7 +5531,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5321,7 +5545,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5335,7 +5559,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5349,7 +5573,7 @@ }, { "c": "110", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5363,7 +5587,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -5377,7 +5601,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5391,7 +5615,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5405,7 +5629,7 @@ }, { "c": "margin", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5419,7 +5643,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5433,7 +5657,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5447,7 +5671,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5461,7 +5685,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5475,7 +5699,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5489,7 +5713,7 @@ }, { "c": "position", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5503,7 +5727,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5517,7 +5741,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5531,7 +5755,7 @@ }, { "c": "relative", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -5545,7 +5769,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5559,7 +5783,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5573,7 +5797,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -5587,7 +5811,7 @@ }, { "c": "intro", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -5601,21 +5825,7 @@ }, { "c": " ", - "t": "source.css", - "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" - } - }, - { - "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5628,10 +5838,66 @@ } }, { - "c": " ", - "t": "source.css meta.property-list.css", + "c": "span", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { - "dark_plus": "default: #D4D4D4", + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D", + "dark_modern": "entity.name.tag.css: #D7BA7D", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": "::", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-element.css punctuation.definition.entity.css", + "r": { + "dark_plus": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.pseudo-element.css: #800000", + "dark_vs": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.pseudo-element.css: #800000", + "hc_black": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.pseudo-element.css: #0F4A85", + "light_modern": "entity.other.attribute-name.pseudo-element.css: #800000" + } + }, + { + "c": "after", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-element.css", + "r": { + "dark_plus": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.pseudo-element.css: #800000", + "dark_vs": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.pseudo-element.css: #800000", + "hc_black": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.pseudo-element.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.pseudo-element.css: #0F4A85", + "light_modern": "entity.other.attribute-name.pseudo-element.css: #800000" + } + }, + { + "c": "{", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.at-rule.body.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", @@ -5643,7 +5909,7 @@ }, { "c": "min-width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5657,7 +5923,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5671,7 +5937,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5685,7 +5951,7 @@ }, { "c": "470", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5699,7 +5965,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -5713,7 +5979,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5727,7 +5993,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5741,7 +6007,7 @@ }, { "c": "width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5755,7 +6021,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5769,7 +6035,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5783,7 +6049,7 @@ }, { "c": "100", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5797,7 +6063,7 @@ }, { "c": "%", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.percentage.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.percentage.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -5811,7 +6077,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5825,7 +6091,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5839,7 +6105,7 @@ }, { "c": "header", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -5853,7 +6119,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5867,7 +6133,7 @@ }, { "c": "h1", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -5881,7 +6147,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5895,7 +6161,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5909,7 +6175,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5923,7 +6189,7 @@ }, { "c": "background", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -5937,7 +6203,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5951,7 +6217,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5965,7 +6231,7 @@ }, { "c": "transparent", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -5979,7 +6245,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5993,7 +6259,7 @@ }, { "c": "url", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", "r": { "dark_plus": "support.function: #DCDCAA", "light_plus": "support.function: #795E26", @@ -6007,7 +6273,7 @@ }, { "c": "(", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6021,7 +6287,7 @@ }, { "c": "h1.gif", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", "r": { "dark_plus": "source.css variable: #9CDCFE", "light_plus": "source.css variable: #E50000", @@ -6035,7 +6301,7 @@ }, { "c": ")", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6049,7 +6315,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6063,7 +6329,7 @@ }, { "c": "no-repeat", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6077,7 +6343,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6091,7 +6357,7 @@ }, { "c": "top", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6105,7 +6371,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6119,7 +6385,7 @@ }, { "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6133,7 +6399,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6147,7 +6413,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6161,7 +6427,7 @@ }, { "c": "margin-top", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6175,7 +6441,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6189,7 +6455,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6203,7 +6469,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6217,7 +6483,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -6231,7 +6497,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6245,7 +6511,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6259,7 +6525,7 @@ }, { "c": "display", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6273,7 +6539,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6287,7 +6553,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6301,7 +6567,7 @@ }, { "c": "block", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6315,7 +6581,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6329,7 +6595,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6343,7 +6609,7 @@ }, { "c": "width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6357,7 +6623,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6371,7 +6637,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6385,7 +6651,7 @@ }, { "c": "219", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6399,7 +6665,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -6413,7 +6679,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6427,7 +6693,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6441,7 +6707,7 @@ }, { "c": "height", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6455,7 +6721,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6469,7 +6735,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6483,7 +6749,7 @@ }, { "c": "87", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6497,7 +6763,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -6511,7 +6777,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6525,7 +6791,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6539,7 +6805,7 @@ }, { "c": "float", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6553,7 +6819,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6567,7 +6833,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6581,7 +6847,7 @@ }, { "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6595,7 +6861,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6609,7 +6875,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6623,7 +6889,7 @@ }, { "c": "text-indent", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6637,7 +6903,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6651,7 +6917,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6665,7 +6931,7 @@ }, { "c": "100", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6679,7 +6945,7 @@ }, { "c": "%", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.percentage.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.percentage.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -6693,7 +6959,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6707,7 +6973,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6721,7 +6987,7 @@ }, { "c": "white-space", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6735,7 +7001,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6749,7 +7015,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6763,7 +7029,7 @@ }, { "c": "nowrap", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6777,7 +7043,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6791,7 +7057,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6805,7 +7071,7 @@ }, { "c": "overflow", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6819,7 +7085,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6833,7 +7099,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6847,7 +7113,7 @@ }, { "c": "hidden", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -6861,7 +7127,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6875,7 +7141,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6889,7 +7155,7 @@ }, { "c": "header", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -6903,7 +7169,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6917,7 +7183,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6931,7 +7197,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6945,7 +7211,7 @@ }, { "c": "padding-top", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -6959,7 +7225,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6973,7 +7239,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6987,7 +7253,7 @@ }, { "c": "20", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7001,7 +7267,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7015,7 +7281,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7029,7 +7295,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7043,7 +7309,7 @@ }, { "c": "height", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7057,7 +7323,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7071,7 +7337,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7085,7 +7351,7 @@ }, { "c": "87", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7099,7 +7365,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7113,7 +7379,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7127,7 +7393,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7141,7 +7407,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -7155,7 +7421,7 @@ }, { "c": "summary", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -7169,7 +7435,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7183,7 +7449,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7197,7 +7463,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7211,7 +7477,7 @@ }, { "c": "clear", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7225,7 +7491,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7239,7 +7505,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7253,7 +7519,7 @@ }, { "c": "both", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -7267,7 +7533,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7281,7 +7547,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7295,7 +7561,7 @@ }, { "c": "margin", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7309,7 +7575,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7323,7 +7589,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7337,7 +7603,7 @@ }, { "c": "20", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7351,7 +7617,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7365,7 +7631,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7379,7 +7645,7 @@ }, { "c": "20", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7393,7 +7659,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7407,7 +7673,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7421,7 +7687,7 @@ }, { "c": "20", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7435,7 +7701,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7449,7 +7715,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7463,7 +7729,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7477,7 +7743,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7491,7 +7757,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7505,7 +7771,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7519,7 +7785,7 @@ }, { "c": "width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7533,7 +7799,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7547,7 +7813,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7561,7 +7827,7 @@ }, { "c": "160", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7575,7 +7841,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7589,7 +7855,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7603,7 +7869,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7617,7 +7883,7 @@ }, { "c": "float", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7631,7 +7897,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7645,7 +7911,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7659,7 +7925,7 @@ }, { "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -7673,7 +7939,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7687,7 +7953,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7701,7 +7967,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -7715,7 +7981,7 @@ }, { "c": "summary", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -7729,7 +7995,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7743,7 +8009,7 @@ }, { "c": "p", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -7757,7 +8023,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7771,7 +8037,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7785,7 +8051,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7799,7 +8065,7 @@ }, { "c": "font", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7813,7 +8079,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7827,7 +8093,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7841,7 +8107,7 @@ }, { "c": "italic", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -7855,7 +8121,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7869,7 +8135,7 @@ }, { "c": "1.1", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7883,7 +8149,7 @@ }, { "c": "em", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.em.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.em.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -7897,7 +8163,7 @@ }, { "c": "/", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7911,7 +8177,7 @@ }, { "c": "2.2", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7925,7 +8191,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7939,7 +8205,7 @@ }, { "c": "georgia", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.font-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.font-name.css", "r": { "dark_plus": "support.constant.font-name: #CE9178", "light_plus": "support.constant.font-name: #0451A5", @@ -7953,7 +8219,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7967,7 +8233,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7981,7 +8247,7 @@ }, { "c": "text-align", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -7995,7 +8261,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8009,7 +8275,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8023,7 +8289,7 @@ }, { "c": "center", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -8037,7 +8303,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8051,7 +8317,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8065,7 +8331,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -8079,7 +8345,7 @@ }, { "c": "preamble", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -8093,7 +8359,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8107,7 +8373,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8121,7 +8387,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8135,7 +8401,7 @@ }, { "c": "clear", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -8149,7 +8415,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8163,7 +8429,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8177,7 +8443,7 @@ }, { "c": "right", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -8191,7 +8457,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8205,7 +8471,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8219,7 +8485,7 @@ }, { "c": "padding", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -8233,7 +8499,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8247,7 +8513,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8261,7 +8527,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8275,7 +8541,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -8289,7 +8555,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8303,7 +8569,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8317,7 +8583,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -8331,7 +8597,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8345,7 +8611,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8359,7 +8625,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8373,7 +8639,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8387,7 +8653,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -8401,7 +8667,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8415,7 +8681,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8429,7 +8695,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -8443,7 +8709,7 @@ }, { "c": "supporting", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -8457,7 +8723,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8471,7 +8737,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8485,7 +8751,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8499,7 +8765,7 @@ }, { "c": "padding-left", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -8513,7 +8779,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8527,7 +8793,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8541,7 +8807,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8555,7 +8821,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -8569,7 +8835,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8583,7 +8849,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8597,7 +8863,7 @@ }, { "c": "margin-bottom", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -8611,7 +8877,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8625,7 +8891,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8639,7 +8905,7 @@ }, { "c": "40", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8653,7 +8919,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -8667,7 +8933,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8681,7 +8947,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8695,7 +8961,7 @@ }, { "c": "#", - "t": "source.css meta.selector.css entity.other.attribute-name.id.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.id.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.id.css: #D7BA7D", "light_plus": "entity.other.attribute-name.id.css: #800000", @@ -8709,7 +8975,7 @@ }, { "c": "footer", - "t": "source.css meta.selector.css entity.other.attribute-name.id.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.id.css", "r": { "dark_plus": "entity.other.attribute-name.id.css: #D7BA7D", "light_plus": "entity.other.attribute-name.id.css: #800000", @@ -8723,7 +8989,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8737,7 +9003,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8751,7 +9017,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8765,7 +9031,7 @@ }, { "c": "text-align", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -8779,7 +9045,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8793,7 +9059,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8807,7 +9073,7 @@ }, { "c": "center", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -8821,7 +9087,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8835,7 +9101,7 @@ }, { "c": "footer", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -8849,7 +9115,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8863,7 +9129,7 @@ }, { "c": "a", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -8877,7 +9143,7 @@ }, { "c": ":", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -8891,7 +9157,7 @@ }, { "c": "link", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -8905,7 +9171,7 @@ }, { "c": ",", - "t": "source.css meta.selector.css punctuation.separator.list.comma.css", + "t": "source.css meta.at-rule.body.css meta.selector.css punctuation.separator.list.comma.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8919,7 +9185,7 @@ }, { "c": "footer", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -8933,7 +9199,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8947,7 +9213,7 @@ }, { "c": "a", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -8961,7 +9227,7 @@ }, { "c": ":", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -8975,7 +9241,7 @@ }, { "c": "visited", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -8989,7 +9255,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9003,7 +9269,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9017,7 +9283,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9031,7 +9297,7 @@ }, { "c": "margin-right", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9045,7 +9311,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9059,7 +9325,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9073,7 +9339,7 @@ }, { "c": "20", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -9087,7 +9353,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -9101,7 +9367,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9115,7 +9381,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9129,7 +9395,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9143,7 +9409,7 @@ }, { "c": "sidebar", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9157,7 +9423,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9171,7 +9437,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9185,7 +9451,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9199,7 +9465,7 @@ }, { "c": "margin-left", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9213,7 +9479,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9227,7 +9493,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9241,7 +9507,7 @@ }, { "c": "600", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -9255,7 +9521,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -9269,7 +9535,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9283,7 +9549,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9297,7 +9563,7 @@ }, { "c": "position", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9311,7 +9577,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9325,7 +9591,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9339,7 +9605,7 @@ }, { "c": "absolute", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -9353,7 +9619,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9367,7 +9633,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9381,7 +9647,7 @@ }, { "c": "top", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9395,7 +9661,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9409,7 +9675,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9423,7 +9689,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -9437,7 +9703,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9451,7 +9717,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9465,7 +9731,7 @@ }, { "c": "right", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9479,7 +9745,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9493,7 +9759,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9507,7 +9773,7 @@ }, { "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -9521,7 +9787,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9535,7 +9801,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9549,7 +9815,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9563,7 +9829,7 @@ }, { "c": "sidebar", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9577,7 +9843,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9591,7 +9857,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9605,7 +9871,7 @@ }, { "c": "wrapper", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -9619,7 +9885,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9633,7 +9899,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9647,7 +9913,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9661,7 +9927,7 @@ }, { "c": "font", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9675,7 +9941,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9689,7 +9955,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9703,7 +9969,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -9717,7 +9983,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -9731,7 +9997,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9745,7 +10011,7 @@ }, { "c": "verdana", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.font-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.font-name.css", "r": { "dark_plus": "support.constant.font-name: #CE9178", "light_plus": "support.constant.font-name: #0451A5", @@ -9759,7 +10025,7 @@ }, { "c": ",", - "t": "source.css meta.property-list.css meta.property-value.css punctuation.separator.list.comma.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css punctuation.separator.list.comma.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9773,7 +10039,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9787,7 +10053,7 @@ }, { "c": "sans-serif", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.font-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.font-name.css", "r": { "dark_plus": "support.constant.font-name: #CE9178", "light_plus": "support.constant.font-name: #0451A5", @@ -9801,7 +10067,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9815,7 +10081,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9829,7 +10095,7 @@ }, { "c": "background", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -9843,7 +10109,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9857,7 +10123,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9871,7 +10137,7 @@ }, { "c": "transparent", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -9885,7 +10151,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9899,7 +10165,7 @@ }, { "c": "url", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", "r": { "dark_plus": "support.function: #DCDCAA", "light_plus": "support.function: #795E26", @@ -9913,7 +10179,7 @@ }, { "c": "(", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9927,7 +10193,7 @@ }, { "c": "paper-bg.jpg", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", "r": { "dark_plus": "source.css variable: #9CDCFE", "light_plus": "source.css variable: #E50000", @@ -9941,7 +10207,7 @@ }, { "c": ")", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9955,7 +10221,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9969,7 +10235,7 @@ }, { "c": "top", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -9983,7 +10249,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9997,7 +10263,7 @@ }, { "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -10011,7 +10277,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10025,7 +10291,7 @@ }, { "c": "repeat-y", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { "dark_plus": "support.constant.property-value: #CE9178", "light_plus": "support.constant.property-value: #0451A5", @@ -10039,7 +10305,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10053,7 +10319,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10067,7 +10333,7 @@ }, { "c": "padding", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10081,7 +10347,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10095,7 +10361,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10109,7 +10375,7 @@ }, { "c": "10", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -10123,7 +10389,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -10137,7 +10403,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10151,7 +10417,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10165,7 +10431,7 @@ }, { "c": "margin-top", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10179,7 +10445,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10193,7 +10459,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10207,7 +10473,7 @@ }, { "c": "150", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -10221,7 +10487,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -10235,7 +10501,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10249,7 +10515,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10263,7 +10529,7 @@ }, { "c": "width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10277,7 +10543,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10291,7 +10557,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10305,7 +10571,7 @@ }, { "c": "130", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -10319,7 +10585,7 @@ }, { "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", "r": { "dark_plus": "keyword.other.unit: #B5CEA8", "light_plus": "keyword.other.unit: #098658", @@ -10333,7 +10599,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10347,7 +10613,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10361,7 +10627,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10375,7 +10641,7 @@ }, { "c": "sidebar", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10389,7 +10655,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10403,7 +10669,7 @@ }, { "c": "li", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -10417,7 +10683,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10431,7 +10697,7 @@ }, { "c": "a", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -10445,7 +10711,7 @@ }, { "c": ":", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -10459,7 +10725,7 @@ }, { "c": "link", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -10473,7 +10739,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10487,7 +10753,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10501,7 +10767,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10515,7 +10781,7 @@ }, { "c": "color", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10529,7 +10795,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10543,7 +10809,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10557,7 +10823,7 @@ }, { "c": "#", - "t": "source.css meta.property-list.css meta.property-value.css constant.other.color.rgb-value.hex.css punctuation.definition.constant.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.other.color.rgb-value.hex.css punctuation.definition.constant.css", "r": { "dark_plus": "constant.other.color.rgb-value: #CE9178", "light_plus": "constant.other.color.rgb-value: #0451A5", @@ -10571,7 +10837,7 @@ }, { "c": "988F5E", - "t": "source.css meta.property-list.css meta.property-value.css constant.other.color.rgb-value.hex.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css constant.other.color.rgb-value.hex.css", "r": { "dark_plus": "constant.other.color.rgb-value: #CE9178", "light_plus": "constant.other.color.rgb-value: #0451A5", @@ -10585,7 +10851,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10599,7 +10865,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10613,7 +10879,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10627,7 +10893,7 @@ }, { "c": "sidebar", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10641,7 +10907,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10655,7 +10921,7 @@ }, { "c": "li", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -10669,7 +10935,7 @@ }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.at-rule.body.css meta.selector.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10683,7 +10949,7 @@ }, { "c": "a", - "t": "source.css meta.selector.css entity.name.tag.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.name.tag.css", "r": { "dark_plus": "entity.name.tag.css: #D7BA7D", "light_plus": "entity.name.tag: #800000", @@ -10697,7 +10963,7 @@ }, { "c": ":", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css punctuation.definition.entity.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -10711,7 +10977,7 @@ }, { "c": "visited", - "t": "source.css meta.selector.css entity.other.attribute-name.pseudo-class.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.pseudo-class.css", "r": { "dark_plus": "source.css entity.other.attribute-name.pseudo-class: #D7BA7D", "light_plus": "source.css entity.other.attribute-name.pseudo-class: #800000", @@ -10725,7 +10991,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10739,7 +11005,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10753,7 +11019,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10767,7 +11033,7 @@ }, { "c": "color", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10781,7 +11047,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10795,7 +11061,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10809,7 +11075,7 @@ }, { "c": "'", - "t": "source.css meta.property-list.css meta.property-value.css string.quoted.single.css punctuation.definition.string.begin.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css string.quoted.single.css punctuation.definition.string.begin.css", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -10823,7 +11089,7 @@ }, { "c": "#B3AE94", - "t": "source.css meta.property-list.css meta.property-value.css string.quoted.single.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css string.quoted.single.css", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -10837,7 +11103,7 @@ }, { "c": "'", - "t": "source.css meta.property-list.css meta.property-value.css string.quoted.single.css punctuation.definition.string.end.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css string.quoted.single.css punctuation.definition.string.end.css", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -10851,7 +11117,7 @@ }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10865,7 +11131,7 @@ }, { "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10879,7 +11145,7 @@ }, { "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10892,8 +11158,8 @@ } }, { - "c": "extra1", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "c": "parent", + "t": "source.css meta.at-rule.body.css meta.selector.css entity.other.attribute-name.class.css", "r": { "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", "light_plus": "entity.other.attribute-name.class.css: #800000", @@ -10907,7 +11173,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.at-rule.body.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10921,7 +11187,7 @@ }, { "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10935,7 +11201,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10948,8 +11214,8 @@ } }, { - "c": "background", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "c": "color", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", "light_plus": "support.type.property-name: #E50000", @@ -10963,7 +11229,7 @@ }, { "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10977,7 +11243,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -10990,22 +11256,22 @@ } }, { - "c": "transparent", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "c": "tomato", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.color.w3c-extended-color-name.css", "r": { - "dark_plus": "support.constant.property-value: #CE9178", - "light_plus": "support.constant.property-value: #0451A5", + "dark_plus": "support.constant.color: #CE9178", + "light_plus": "support.constant.color: #0451A5", "dark_vs": "default: #D4D4D4", - "light_vs": "support.constant.property-value: #0451A5", - "hc_black": "support.constant.property-value: #CE9178", - "dark_modern": "support.constant.property-value: #CE9178", - "hc_light": "support.constant.property-value: #0451A5", - "light_modern": "support.constant.property-value: #0451A5" + "light_vs": "support.constant.color: #0451A5", + "hc_black": "support.constant.color: #CE9178", + "dark_modern": "support.constant.color: #CE9178", + "hc_light": "support.constant.color: #0451A5", + "light_modern": "support.constant.color: #0451A5" } }, { - "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "c": ";", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11018,22 +11284,22 @@ } }, { - "c": "url", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", + "c": " .", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "dark_modern": "support.function: #DCDCAA", - "hc_light": "support.function: #5E2CBC", - "light_modern": "support.function: #795E26" + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "(", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", + "c": "child", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11046,22 +11312,8 @@ } }, { - "c": "cr2.gif", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", - "r": { - "dark_plus": "source.css variable: #9CDCFE", - "light_plus": "source.css variable: #E50000", - "dark_vs": "source.css variable: #9CDCFE", - "light_vs": "source.css variable: #E50000", - "hc_black": "source.css variable: #D4D4D4", - "dark_modern": "source.css variable: #9CDCFE", - "hc_light": "source.css variable: #264F78", - "light_modern": "source.css variable: #E50000" - } - }, - { - "c": ")", - "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", + "c": " {", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11074,8 +11326,8 @@ } }, { - "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "c": " ", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11088,22 +11340,22 @@ } }, { - "c": "top", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "c": "color", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { - "dark_plus": "support.constant.property-value: #CE9178", - "light_plus": "support.constant.property-value: #0451A5", - "dark_vs": "default: #D4D4D4", - "light_vs": "support.constant.property-value: #0451A5", - "hc_black": "support.constant.property-value: #CE9178", - "dark_modern": "support.constant.property-value: #CE9178", - "hc_light": "support.constant.property-value: #0451A5", - "light_modern": "support.constant.property-value: #0451A5" + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" } }, { - "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "c": ":", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11115,23 +11367,9 @@ "light_modern": "default: #3B3B3B" } }, - { - "c": "left", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", - "r": { - "dark_plus": "support.constant.property-value: #CE9178", - "light_plus": "support.constant.property-value: #0451A5", - "dark_vs": "default: #D4D4D4", - "light_vs": "support.constant.property-value: #0451A5", - "hc_black": "support.constant.property-value: #CE9178", - "dark_modern": "support.constant.property-value: #CE9178", - "hc_light": "support.constant.property-value: #0451A5", - "light_modern": "support.constant.property-value: #0451A5" - } - }, { "c": " ", - "t": "source.css meta.property-list.css meta.property-value.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11144,22 +11382,22 @@ } }, { - "c": "no-repeat", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "c": "blue", + "t": "source.css meta.at-rule.body.css meta.property-list.css meta.property-value.css support.constant.color.w3c-standard-color-name.css", "r": { - "dark_plus": "support.constant.property-value: #CE9178", - "light_plus": "support.constant.property-value: #0451A5", + "dark_plus": "support.constant.color: #CE9178", + "light_plus": "support.constant.color: #0451A5", "dark_vs": "default: #D4D4D4", - "light_vs": "support.constant.property-value: #0451A5", - "hc_black": "support.constant.property-value: #CE9178", - "dark_modern": "support.constant.property-value: #CE9178", - "hc_light": "support.constant.property-value: #0451A5", - "light_modern": "support.constant.property-value: #0451A5" + "light_vs": "support.constant.color: #0451A5", + "hc_black": "support.constant.color: #CE9178", + "dark_modern": "support.constant.color: #CE9178", + "hc_light": "support.constant.color: #0451A5", + "light_modern": "support.constant.color: #0451A5" } }, { "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11173,7 +11411,7 @@ }, { "c": " ", - "t": "source.css meta.property-list.css", + "t": "source.css meta.at-rule.body.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11186,22 +11424,8 @@ } }, { - "c": "position", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", - "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #E50000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #E50000", - "hc_black": "support.type.property-name: #D4D4D4", - "dark_modern": "support.type.property-name: #9CDCFE", - "hc_light": "support.type.property-name: #264F78", - "light_modern": "support.type.property-name: #E50000" - } - }, - { - "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "c": "}", + "t": "source.css meta.at-rule.body.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11214,8 +11438,8 @@ } }, { - "c": " ", - "t": "source.css meta.property-list.css", + "c": "}", + "t": "source.css meta.at-rule.body.css punctuation.section.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11228,22 +11452,50 @@ } }, { - "c": "absolute", - "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "c": ".", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { - "dark_plus": "support.constant.property-value: #CE9178", - "light_plus": "support.constant.property-value: #0451A5", + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": "parent", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "r": { + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", "dark_vs": "default: #D4D4D4", - "light_vs": "support.constant.property-value: #0451A5", - "hc_black": "support.constant.property-value: #CE9178", - "dark_modern": "support.constant.property-value: #CE9178", - "hc_light": "support.constant.property-value: #0451A5", - "light_modern": "support.constant.property-value: #0451A5" + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11270,7 +11522,7 @@ } }, { - "c": "top", + "c": "color", "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", @@ -11312,36 +11564,36 @@ } }, { - "c": "40", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "c": "tomato", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.color.w3c-extended-color-name.css", "r": { - "dark_plus": "constant.numeric: #B5CEA8", - "light_plus": "constant.numeric: #098658", - "dark_vs": "constant.numeric: #B5CEA8", - "light_vs": "constant.numeric: #098658", - "hc_black": "constant.numeric: #B5CEA8", - "dark_modern": "constant.numeric: #B5CEA8", - "hc_light": "constant.numeric: #096D48", - "light_modern": "constant.numeric: #098658" + "dark_plus": "support.constant.color: #CE9178", + "light_plus": "support.constant.color: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.color: #0451A5", + "hc_black": "support.constant.color: #CE9178", + "dark_modern": "support.constant.color: #CE9178", + "hc_light": "support.constant.color: #0451A5", + "light_modern": "support.constant.color: #0451A5" } }, { - "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", "r": { - "dark_plus": "keyword.other.unit: #B5CEA8", - "light_plus": "keyword.other.unit: #098658", - "dark_vs": "keyword.other.unit: #B5CEA8", - "light_vs": "keyword.other.unit: #098658", - "hc_black": "keyword.other.unit: #B5CEA8", - "dark_modern": "keyword.other.unit: #B5CEA8", - "hc_light": "keyword.other.unit: #096D48", - "light_modern": "keyword.other.unit: #098658" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "c": " & .", + "t": "source.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11354,7 +11606,21 @@ } }, { - "c": " ", + "c": "child", + "t": "source.css meta.property-list.css meta.property-name.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " {", "t": "source.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", @@ -11368,7 +11634,21 @@ } }, { - "c": "right", + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "color", "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", @@ -11410,17 +11690,17 @@ } }, { - "c": "0", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "c": "blue", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.color.w3c-standard-color-name.css", "r": { - "dark_plus": "constant.numeric: #B5CEA8", - "light_plus": "constant.numeric: #098658", - "dark_vs": "constant.numeric: #B5CEA8", - "light_vs": "constant.numeric: #098658", - "hc_black": "constant.numeric: #B5CEA8", - "dark_modern": "constant.numeric: #B5CEA8", - "hc_light": "constant.numeric: #096D48", - "light_modern": "constant.numeric: #098658" + "dark_plus": "support.constant.color: #CE9178", + "light_plus": "support.constant.color: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.color: #0451A5", + "hc_black": "support.constant.color: #CE9178", + "dark_modern": "support.constant.color: #CE9178", + "hc_light": "support.constant.color: #0451A5", + "light_modern": "support.constant.color: #0451A5" } }, { @@ -11452,22 +11732,8 @@ } }, { - "c": "width", - "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", - "r": { - "dark_plus": "support.type.property-name: #9CDCFE", - "light_plus": "support.type.property-name: #E50000", - "dark_vs": "support.type.property-name: #9CDCFE", - "light_vs": "support.type.property-name: #E50000", - "hc_black": "support.type.property-name: #D4D4D4", - "dark_modern": "support.type.property-name: #9CDCFE", - "hc_light": "support.type.property-name: #264F78", - "light_modern": "support.type.property-name: #E50000" - } - }, - { - "c": ":", - "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11480,8 +11746,8 @@ } }, { - "c": " ", - "t": "source.css meta.property-list.css", + "c": "}", + "t": "source.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11494,36 +11760,50 @@ } }, { - "c": "148", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "c": ".", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", "r": { - "dark_plus": "constant.numeric: #B5CEA8", - "light_plus": "constant.numeric: #098658", - "dark_vs": "constant.numeric: #B5CEA8", - "light_vs": "constant.numeric: #098658", - "hc_black": "constant.numeric: #B5CEA8", - "dark_modern": "constant.numeric: #B5CEA8", - "hc_light": "constant.numeric: #096D48", - "light_modern": "constant.numeric: #098658" + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" } }, { - "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "c": "extra1", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css", "r": { - "dark_plus": "keyword.other.unit: #B5CEA8", - "light_plus": "keyword.other.unit: #098658", - "dark_vs": "keyword.other.unit: #B5CEA8", - "light_vs": "keyword.other.unit: #098658", - "hc_black": "keyword.other.unit: #B5CEA8", - "dark_modern": "keyword.other.unit: #B5CEA8", - "hc_light": "keyword.other.unit: #096D48", - "light_modern": "keyword.other.unit: #098658" + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" } }, { - "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11550,7 +11830,7 @@ } }, { - "c": "height", + "c": "background", "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", @@ -11592,36 +11872,22 @@ } }, { - "c": "110", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "c": "transparent", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { - "dark_plus": "constant.numeric: #B5CEA8", - "light_plus": "constant.numeric: #098658", - "dark_vs": "constant.numeric: #B5CEA8", - "light_vs": "constant.numeric: #098658", - "hc_black": "constant.numeric: #B5CEA8", - "dark_modern": "constant.numeric: #B5CEA8", - "hc_light": "constant.numeric: #096D48", - "light_modern": "constant.numeric: #098658" + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" } }, { - "c": "px", - "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", - "r": { - "dark_plus": "keyword.other.unit: #B5CEA8", - "light_plus": "keyword.other.unit: #098658", - "dark_vs": "keyword.other.unit: #B5CEA8", - "light_vs": "keyword.other.unit: #098658", - "hc_black": "keyword.other.unit: #B5CEA8", - "dark_modern": "keyword.other.unit: #B5CEA8", - "hc_light": "keyword.other.unit: #096D48", - "light_modern": "keyword.other.unit: #098658" - } - }, - { - "c": ";", - "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "c": " ", + "t": "source.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11634,8 +11900,22 @@ } }, { - "c": "}", - "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "c": "url", + "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css support.function.url.css", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.function: #DCDCAA", + "dark_modern": "support.function: #DCDCAA", + "hc_light": "support.function: #5E2CBC", + "light_modern": "support.function: #795E26" + } + }, + { + "c": "(", + "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.begin.bracket.round.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11648,36 +11928,36 @@ } }, { - "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "c": "cr2.gif", + "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css variable.parameter.url.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "source.css variable: #9CDCFE", + "light_plus": "source.css variable: #E50000", + "dark_vs": "source.css variable: #9CDCFE", + "light_vs": "source.css variable: #E50000", + "hc_black": "source.css variable: #D4D4D4", + "dark_modern": "source.css variable: #9CDCFE", + "hc_light": "source.css variable: #264F78", + "light_modern": "source.css variable: #E50000" } }, { - "c": "chat-feature-container", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "c": ")", + "t": "source.css meta.property-list.css meta.property-value.css meta.function.url.css punctuation.section.function.end.bracket.round.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": " ", - "t": "source.css meta.selector.css", + "t": "source.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11690,36 +11970,22 @@ } }, { - "c": ".", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", - "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" - } - }, - { - "c": "codicon", - "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "c": "top", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" } }, { - "c": "[", - "t": "source.css meta.selector.css meta.attribute-selector.css punctuation.definition.entity.begin.bracket.square.css", + "c": " ", + "t": "source.css meta.property-list.css meta.property-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11732,78 +11998,92 @@ } }, { - "c": "class", - "t": "source.css meta.selector.css meta.attribute-selector.css entity.other.attribute-name.css", + "c": "left", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { - "dark_plus": "entity.other.attribute-name: #9CDCFE", - "light_plus": "entity.other.attribute-name: #E50000", - "dark_vs": "entity.other.attribute-name: #9CDCFE", - "light_vs": "entity.other.attribute-name: #E50000", - "hc_black": "entity.other.attribute-name: #9CDCFE", - "dark_modern": "entity.other.attribute-name: #9CDCFE", - "hc_light": "entity.other.attribute-name: #264F78", - "light_modern": "entity.other.attribute-name: #E50000" + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" } }, { - "c": "*=", - "t": "source.css meta.selector.css meta.attribute-selector.css keyword.operator.pattern.css", + "c": " ", + "t": "source.css meta.property-list.css meta.property-value.css", "r": { - "dark_plus": "keyword.operator: #D4D4D4", - "light_plus": "keyword.operator: #000000", - "dark_vs": "keyword.operator: #D4D4D4", - "light_vs": "keyword.operator: #000000", - "hc_black": "keyword.operator: #D4D4D4", - "dark_modern": "keyword.operator: #D4D4D4", - "hc_light": "keyword.operator: #000000", - "light_modern": "keyword.operator: #000000" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "'", - "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css punctuation.definition.string.begin.css", + "c": "no-repeat", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" } }, { - "c": "codicon-", - "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css", + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "'", - "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css punctuation.definition.string.end.css", + "c": " ", + "t": "source.css meta.property-list.css", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "]", - "t": "source.css meta.selector.css meta.attribute-selector.css punctuation.definition.entity.end.bracket.square.css", + "c": "position", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11817,7 +12097,7 @@ }, { "c": " ", - "t": "source.css", + "t": "source.css meta.property-list.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11830,8 +12110,22 @@ } }, { - "c": "{", - "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "c": "absolute", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "r": { + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -11858,7 +12152,7 @@ } }, { - "c": "font-size", + "c": "top", "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", @@ -11900,7 +12194,7 @@ } }, { - "c": "16", + "c": "40", "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", "r": { "dark_plus": "constant.numeric: #B5CEA8", @@ -11941,6 +12235,1504 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "right", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "0", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "width", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "148", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "height", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "110", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ".", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "r": { + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": "chat-feature-container", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "r": { + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": " ", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ".", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css punctuation.definition.entity.css", + "r": { + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": "codicon", + "t": "source.css meta.selector.css entity.other.attribute-name.class.css", + "r": { + "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", + "light_plus": "entity.other.attribute-name.class.css: #800000", + "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", + "light_vs": "entity.other.attribute-name.class.css: #800000", + "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", + "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", + "hc_light": "entity.other.attribute-name.class.css: #0F4A85", + "light_modern": "entity.other.attribute-name.class.css: #800000" + } + }, + { + "c": "[", + "t": "source.css meta.selector.css meta.attribute-selector.css punctuation.definition.entity.begin.bracket.square.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "class", + "t": "source.css meta.selector.css meta.attribute-selector.css entity.other.attribute-name.css", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #E50000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #E50000", + "hc_black": "entity.other.attribute-name: #9CDCFE", + "dark_modern": "entity.other.attribute-name: #9CDCFE", + "hc_light": "entity.other.attribute-name: #264F78", + "light_modern": "entity.other.attribute-name: #E50000" + } + }, + { + "c": "*=", + "t": "source.css meta.selector.css meta.attribute-selector.css keyword.operator.pattern.css", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "'", + "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css punctuation.definition.string.begin.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "codicon-", + "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.css meta.selector.css meta.attribute-selector.css string.quoted.single.css punctuation.definition.string.end.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "]", + "t": "source.css meta.selector.css meta.attribute-selector.css punctuation.definition.entity.end.bracket.square.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "font-size", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "16", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "figma-help-bubble", + "t": "source.css meta.selector.css entity.name.tag.custom.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "position", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "absolute", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "r": { + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "right", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "16", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "bottom", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "16", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "figma-select", + "t": "source.css meta.selector.css entity.name.tag.custom.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": "::part(listbox", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ") ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "max-height", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "250", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "px", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.px.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "div", + "t": "source.css meta.selector.css entity.name.tag.css", + "r": { + "dark_plus": "entity.name.tag.css: #D7BA7D", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag.css: #D7BA7D", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag.css: #D7BA7D", + "dark_modern": "entity.name.tag.css: #D7BA7D", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ">", + "t": "source.css meta.selector.css keyword.operator.combinator.css", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "*", + "t": "source.css meta.selector.css entity.name.tag.wildcard.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "+", + "t": "source.css meta.selector.css keyword.operator.combinator.css", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.css meta.selector.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "*", + "t": "source.css meta.selector.css entity.name.tag.wildcard.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "margin-top", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "4", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "rem", + "t": "source.css meta.property-list.css meta.property-value.css constant.numeric.css keyword.other.unit.rem.css", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "*", + "t": "source.css meta.selector.css entity.name.tag.wildcard.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "box-sizing", + "t": "source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" + } + }, + { + "c": ":", + "t": "source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.css meta.property-list.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "border-box", + "t": "source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "r": { + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" + } + }, + { + "c": ";", + "t": "source.css meta.property-list.css punctuation.terminator.rule.css", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, { "c": "}", "t": "source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", diff --git a/code/extensions/vscode-colorize-tests/test/colorize-results/test_regexp.ts.json b/code/extensions/vscode-colorize-tests/test/colorize-results/test_regexp.ts.json index 3db5004ad54..3a792cdb289 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-results/test_regexp.ts.json +++ b/code/extensions/vscode-colorize-tests/test/colorize-results/test_regexp.ts.json @@ -1581,6 +1581,1140 @@ "light_modern": "string.regexp: #811F3F" } }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "source.ts meta.var.expr.ts storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "g", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.definition.variable.ts variable.other.constant.ts", + "r": { + "dark_plus": "variable.other.constant: #4FC1FF", + "light_plus": "variable.other.constant: #0070C1", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable.other.constant: #4FC1FF", + "hc_light": "variable.other.constant: #02715D", + "light_modern": "variable.other.constant: #0070C1" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.var.expr.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "/", + "t": "source.ts meta.var.expr.ts string.regexp.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "?<=", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.assertion.regexp meta.assertion.look-behind.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "^", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp keyword.control.anchor.regexp", + "r": { + "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", + "light_plus": "keyword.control.anchor.regexp: #EE0000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control.anchor.regexp: #DCDCAA", + "hc_light": "keyword.control.anchor.regexp: #EE0000", + "light_modern": "keyword.control.anchor.regexp: #EE0000" + } + }, + { + "c": "|", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp keyword.operator.or.regexp", + "r": { + "dark_plus": "keyword.operator.or.regexp: #DCDCAA", + "light_plus": "keyword.operator.or.regexp: #EE0000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.or.regexp: #DCDCAA", + "hc_light": "keyword.operator.or.regexp: #EE0000", + "light_modern": "keyword.operator.or.regexp: #EE0000" + } + }, + { + "c": "\\s", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp constant.other.character-class.regexp", + "r": { + "dark_plus": "constant.other.character-class.regexp: #D16969", + "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "constant.other.character-class.regexp: #D16969", + "hc_light": "constant.other.character-class.regexp: #811F3F", + "light_modern": "constant.other.character-class.regexp: #811F3F" + } + }, + { + "c": ")(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "?=", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "[", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "a-z", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp constant.other.character-class.set.regexp constant.other.character-class.range.regexp", + "r": { + "dark_plus": "constant.other.character-class.set.regexp: #D16969", + "light_plus": "constant.other.character-class.set.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "constant.other.character-class.set.regexp: #D16969", + "hc_light": "constant.other.character-class.set.regexp: #811F3F", + "light_modern": "constant.other.character-class.set.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": ")", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "[", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "a-z", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp constant.other.character-class.range.regexp", + "r": { + "dark_plus": "constant.other.character-class.set.regexp: #D16969", + "light_plus": "constant.other.character-class.set.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "constant.other.character-class.set.regexp: #D16969", + "hc_light": "constant.other.character-class.set.regexp: #811F3F", + "light_modern": "constant.other.character-class.set.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": ")", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "?=", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": ".", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp constant.other.character-class.regexp", + "r": { + "dark_plus": "constant.other.character-class.regexp: #D16969", + "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "constant.other.character-class.regexp: #D16969", + "hc_light": "constant.other.character-class.regexp: #811F3F", + "light_modern": "constant.other.character-class.regexp: #811F3F" + } + }, + { + "c": "*", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "\\1", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp keyword.other.back-reference.regexp", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": "$", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp keyword.control.anchor.regexp", + "r": { + "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", + "light_plus": "keyword.control.anchor.regexp: #EE0000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control.anchor.regexp: #DCDCAA", + "hc_light": "keyword.control.anchor.regexp: #EE0000", + "light_modern": "keyword.control.anchor.regexp: #EE0000" + } + }, + { + "c": ")", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "\\(", + "t": "source.ts meta.var.expr.ts string.regexp.ts constant.character.escape.backslash.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "[", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "^", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp keyword.operator.negation.regexp", + "r": { + "dark_plus": "keyword.operator.negation.regexp: #CE9178", + "light_plus": "keyword.operator.negation.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.negation.regexp: #CE9178", + "hc_light": "keyword.operator.negation.regexp: #D16969", + "light_modern": "keyword.operator.negation.regexp: #D16969" + } + }, + { + "c": "()", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp", + "r": { + "dark_plus": "constant.other.character-class.set.regexp: #D16969", + "light_plus": "constant.other.character-class.set.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "constant.other.character-class.set.regexp: #D16969", + "hc_light": "constant.other.character-class.set.regexp: #811F3F", + "light_modern": "constant.other.character-class.set.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp constant.other.character-class.set.regexp punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "*", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "0", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "+", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": ")", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(", + "t": "source.ts meta.var.expr.ts string.regexp.ts meta.group.assertion.regexp punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "?", + "t": "meta.selector.css keyword.operator.combinator.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" } }, { - "c": ".", - "t": "meta.selector.css entity.other.attribute-name.class.css", + "c": "*", + "t": "meta.selector.css entity.name.tag.wildcard.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" } }, { - "c": "codicon", - "t": "meta.selector.css entity.other.attribute-name.class.css", + "c": "+", + "t": "meta.selector.css keyword.operator.combinator.css", "r": { - "dark_plus": "entity.other.attribute-name.class.css: #D7BA7D", - "light_plus": "entity.other.attribute-name.class.css: #800000", - "dark_vs": "entity.other.attribute-name.class.css: #D7BA7D", - "light_vs": "entity.other.attribute-name.class.css: #800000", - "hc_black": "entity.other.attribute-name.class.css: #D7BA7D", - "dark_modern": "entity.other.attribute-name.class.css: #D7BA7D", - "hc_light": "entity.other.attribute-name.class.css: #0F4A85", - "light_modern": "entity.other.attribute-name.class.css: #800000" + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" } }, { - "c": "[", - "t": "meta.selector.css punctuation.css", + "c": "*", + "t": "meta.selector.css entity.name.tag.wildcard.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, + { + "c": "{", + "t": "punctuation.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7616,64 +8988,64 @@ } }, { - "c": "class", - "t": "meta.selector.css entity.other.attribute-name.css", + "c": "margin-top", + "t": "support.type.property-name.css", "r": { - "dark_plus": "entity.other.attribute-name: #9CDCFE", - "light_plus": "entity.other.attribute-name: #E50000", - "dark_vs": "entity.other.attribute-name: #9CDCFE", - "light_vs": "entity.other.attribute-name: #E50000", - "hc_black": "entity.other.attribute-name: #9CDCFE", - "dark_modern": "entity.other.attribute-name: #9CDCFE", - "hc_light": "entity.other.attribute-name: #264F78", - "light_modern": "entity.other.attribute-name: #E50000" + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #E50000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #E50000", + "hc_black": "support.type.property-name: #D4D4D4", + "dark_modern": "support.type.property-name: #9CDCFE", + "hc_light": "support.type.property-name: #264F78", + "light_modern": "support.type.property-name: #E50000" } }, { - "c": "*=", - "t": "meta.selector.css keyword.operator.css", + "c": ":", + "t": "", "r": { - "dark_plus": "keyword.operator: #D4D4D4", - "light_plus": "keyword.operator: #000000", - "dark_vs": "keyword.operator: #D4D4D4", - "light_vs": "keyword.operator: #000000", - "hc_black": "keyword.operator: #D4D4D4", - "dark_modern": "keyword.operator: #D4D4D4", - "hc_light": "keyword.operator: #000000", - "light_modern": "keyword.operator: #000000" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "'", - "t": "meta.selector.css string.quoted.single.css", + "c": "rem", + "t": "constant.numeric.css keyword.other.unit.css", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #098658", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #098658", + "hc_black": "keyword.other.unit: #B5CEA8", + "dark_modern": "keyword.other.unit: #B5CEA8", + "hc_light": "keyword.other.unit: #096D48", + "light_modern": "keyword.other.unit: #098658" } }, { - "c": "'", - "t": "meta.selector.css string.quoted.single.css", + "c": ";", + "t": "", "r": { - "dark_plus": "string: #CE9178", - "light_plus": "string: #A31515", - "dark_vs": "string: #CE9178", - "light_vs": "string: #A31515", - "hc_black": "string: #CE9178", - "dark_modern": "string: #CE9178", - "hc_light": "string: #0F4A85", - "light_modern": "string: #A31515" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { - "c": "]", - "t": "meta.selector.css punctuation.css", + "c": "}", + "t": "punctuation.css", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7685,6 +9057,20 @@ "light_modern": "default: #3B3B3B" } }, + { + "c": "*", + "t": "meta.selector.css entity.name.tag.wildcard.css", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "dark_modern": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #0F4A85", + "light_modern": "entity.name.tag: #800000" + } + }, { "c": "{", "t": "punctuation.css", @@ -7700,7 +9086,7 @@ } }, { - "c": "font-size", + "c": "box-sizing", "t": "support.type.property-name.css", "r": { "dark_plus": "support.type.property-name: #9CDCFE", @@ -7728,17 +9114,17 @@ } }, { - "c": "px", - "t": "constant.numeric.css keyword.other.unit.css", + "c": "border-box", + "t": "support.constant.property-value.css", "r": { - "dark_plus": "keyword.other.unit: #B5CEA8", - "light_plus": "keyword.other.unit: #098658", - "dark_vs": "keyword.other.unit: #B5CEA8", - "light_vs": "keyword.other.unit: #098658", - "hc_black": "keyword.other.unit: #B5CEA8", - "dark_modern": "keyword.other.unit: #B5CEA8", - "hc_light": "keyword.other.unit: #096D48", - "light_modern": "keyword.other.unit: #098658" + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "default: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178", + "dark_modern": "support.constant.property-value: #CE9178", + "hc_light": "support.constant.property-value: #0451A5", + "light_modern": "support.constant.property-value: #0451A5" } }, { diff --git a/code/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_regexp.ts.json b/code/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_regexp.ts.json index 864a8b2af01..e296b24c2ad 100644 --- a/code/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_regexp.ts.json +++ b/code/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_regexp.ts.json @@ -57,21 +57,21 @@ }, { "c": "\\\\", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { "c": "\\x", - "t": "string.regexp.ts internal.regexp constant.character.numeric.regexp", + "t": "string.regexp.ts constant.character.escape.regexp internal.regexp constant.character.numeric.regexp", "r": { "dark_plus": "constant.character: #569CD6", "light_plus": "constant.character: #0000FF", @@ -253,16 +253,16 @@ }, { "c": "\\-", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -393,30 +393,30 @@ }, { "c": "\\]", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { "c": "\\\\", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -659,30 +659,30 @@ }, { "c": "\\/", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { "c": "\\/", - "t": "string.regexp.ts", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "string.regexp: #D16969", - "light_plus": "string.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "string.regexp: #D16969", - "hc_light": "string.regexp: #811F3F", - "light_modern": "string.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -1163,16 +1163,16 @@ }, { "c": "\\s", - "t": "string.regexp.ts constant.other.character-class.regexp", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "constant.other.character-class.regexp: #D16969", - "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "constant.other.character-class.regexp: #D16969", - "hc_light": "constant.other.character-class.regexp: #811F3F", - "light_modern": "constant.other.character-class.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -1233,16 +1233,16 @@ }, { "c": "\\s", - "t": "string.regexp.ts constant.other.character-class.regexp", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "constant.other.character-class.regexp: #D16969", - "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "constant.other.character-class.regexp: #D16969", - "hc_light": "constant.other.character-class.regexp: #811F3F", - "light_modern": "constant.other.character-class.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -1289,16 +1289,16 @@ }, { "c": "\\s", - "t": "string.regexp.ts constant.other.character-class.regexp", + "t": "string.regexp.ts constant.character.escape.regexp", "r": { - "dark_plus": "constant.other.character-class.regexp: #D16969", - "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "constant.other.character-class.regexp: #D16969", - "hc_light": "constant.other.character-class.regexp: #811F3F", - "light_modern": "constant.other.character-class.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -1625,7 +1625,7 @@ }, { "c": "\\u", - "t": "string.regexp.ts internal.regexp constant.character.numeric.regexp", + "t": "string.regexp.ts constant.character.escape.regexp internal.regexp constant.character.numeric.regexp", "r": { "dark_plus": "constant.character: #569CD6", "light_plus": "constant.character: #0000FF", @@ -1821,16 +1821,16 @@ }, { "c": "(?<", - "t": "string.regexp.ts punctuation.definition.group.regexp punctuation.definition.group.assertion.regexp", + "t": "string.regexp.ts punctuation.definition.group.regexp", "r": { - "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", - "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", "hc_black": "string.regexp: #D16969", - "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", - "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", - "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" } }, { @@ -1877,30 +1877,30 @@ }, { "c": "\\s", - "t": "string.regexp.ts punctuation.definition.group.regexp constant.other.character-class.regexp", + "t": "string.regexp.ts punctuation.definition.group.regexp constant.character.escape.regexp", "r": { - "dark_plus": "constant.other.character-class.regexp: #D16969", - "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "constant.other.character-class.regexp: #D16969", - "hc_light": "constant.other.character-class.regexp: #811F3F", - "light_modern": "constant.other.character-class.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { "c": "\\S", - "t": "string.regexp.ts punctuation.definition.group.regexp constant.other.character-class.regexp", + "t": "string.regexp.ts punctuation.definition.group.regexp constant.character.escape.regexp", "r": { - "dark_plus": "constant.other.character-class.regexp: #D16969", - "light_plus": "constant.other.character-class.regexp: #811F3F", + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string.regexp: #D16969", "light_vs": "string.regexp: #811F3F", - "hc_black": "string.regexp: #D16969", - "dark_modern": "constant.other.character-class.regexp: #D16969", - "hc_light": "constant.other.character-class.regexp: #811F3F", - "light_modern": "constant.other.character-class.regexp: #811F3F" + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" } }, { @@ -2127,6 +2127,1532 @@ "light_modern": "string.regexp: #811F3F" } }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "g", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "/", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "(?<", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-behind.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "=", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "^", + "t": "string.regexp.ts keyword.control.anchor.regexp", + "r": { + "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", + "light_plus": "keyword.control.anchor.regexp: #EE0000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control.anchor.regexp: #DCDCAA", + "hc_light": "keyword.control.anchor.regexp: #EE0000", + "light_modern": "keyword.control.anchor.regexp: #EE0000" + } + }, + { + "c": "|", + "t": "string.regexp.ts keyword.operator.or.regexp", + "r": { + "dark_plus": "keyword.operator.or.regexp: #DCDCAA", + "light_plus": "keyword.operator.or.regexp: #EE0000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.or.regexp: #DCDCAA", + "hc_light": "keyword.operator.or.regexp: #EE0000", + "light_modern": "keyword.operator.or.regexp: #EE0000" + } + }, + { + "c": "\\s", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(?", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "=", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "[", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "a", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "-", + "t": "string.regexp.ts constant.other.character-class.range.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "z", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "[", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "a", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "-", + "t": "string.regexp.ts constant.other.character-class.range.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "z", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(?", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "=", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": ".", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "*", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "\\1", + "t": "string.regexp.ts keyword.other.back-reference.regexp", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": "$", + "t": "string.regexp.ts keyword.control.anchor.regexp", + "r": { + "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", + "light_plus": "keyword.control.anchor.regexp: #EE0000", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control.anchor.regexp: #DCDCAA", + "hc_light": "keyword.control.anchor.regexp: #EE0000", + "light_modern": "keyword.control.anchor.regexp: #EE0000" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "\\(", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "(", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "[", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "^", + "t": "string.regexp.ts keyword.operator.negation.regexp", + "r": { + "dark_plus": "keyword.operator.negation.regexp: #CE9178", + "light_plus": "keyword.operator.negation.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.negation.regexp: #CE9178", + "hc_light": "keyword.operator.negation.regexp: #D16969", + "light_modern": "keyword.operator.negation.regexp: #D16969" + } + }, + { + "c": "(", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": ")", + "t": "string.regexp.ts constant.character-class.regexp", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "]", + "t": "string.regexp.ts punctuation.definition.character-class.regexp", + "r": { + "dark_plus": "punctuation.definition.character-class.regexp: #CE9178", + "light_plus": "punctuation.definition.character-class.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.character-class.regexp: #CE9178", + "hc_light": "punctuation.definition.character-class.regexp: #D16969", + "light_modern": "punctuation.definition.character-class.regexp: #D16969" + } + }, + { + "c": "*", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "0", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "+", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(?<", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-behind.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "!", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "p", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "a", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "s", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "s", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "w", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "o", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "r", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "d", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "|", + "t": "string.regexp.ts keyword.operator.or.regexp", + "r": { + "dark_plus": "keyword.operator.or.regexp: #DCDCAA", + "light_plus": "keyword.operator.or.regexp: #EE0000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.or.regexp: #DCDCAA", + "hc_light": "keyword.operator.or.regexp: #EE0000", + "light_modern": "keyword.operator.or.regexp: #EE0000" + } + }, + { + "c": "t", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "o", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "k", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "e", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "n", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "\\)", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "(?", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "!", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": ".", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "*", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "?", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "(", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "p", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "a", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "s", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "s", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "w", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "o", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "r", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "d", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "|", + "t": "string.regexp.ts keyword.operator.or.regexp", + "r": { + "dark_plus": "keyword.operator.or.regexp: #DCDCAA", + "light_plus": "keyword.operator.or.regexp: #EE0000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.or.regexp: #DCDCAA", + "hc_light": "keyword.operator.or.regexp: #EE0000", + "light_modern": "keyword.operator.or.regexp: #EE0000" + } + }, + { + "c": "t", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "o", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "k", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "e", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "n", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "{", + "t": "string.regexp.ts constant.character.escape.regexp punctuation.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "L", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "}", + "t": "string.regexp.ts constant.character.escape.regexp punctuation.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "(?:", + "t": "string.regexp.ts punctuation.definition.group.regexp punctuation.definition.group.no-capture.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(?<", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-behind.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "=", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "\\(", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "\\d", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": "{", + "t": "string.regexp.ts keyword.operator.quantifier.regexp punctuation.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "3", + "t": "string.regexp.ts keyword.operator.quantifier.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "}", + "t": "string.regexp.ts keyword.operator.quantifier.regexp punctuation.regexp", + "r": { + "dark_plus": "keyword.operator.quantifier.regexp: #D7BA7D", + "light_plus": "keyword.operator.quantifier.regexp: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.quantifier.regexp: #D7BA7D", + "hc_light": "keyword.operator.quantifier.regexp: #000000", + "light_modern": "keyword.operator.quantifier.regexp: #000000" + } + }, + { + "c": "\\)", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "-", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "\\1", + "t": "string.regexp.ts keyword.other.back-reference.regexp", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": "|", + "t": "string.regexp.ts keyword.operator.or.regexp", + "r": { + "dark_plus": "keyword.operator.or.regexp: #DCDCAA", + "light_plus": "keyword.operator.or.regexp: #EE0000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator.or.regexp: #DCDCAA", + "hc_light": "keyword.operator.or.regexp: #EE0000", + "light_modern": "keyword.operator.or.regexp: #EE0000" + } + }, + { + "c": "-", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "\\1", + "t": "string.regexp.ts keyword.other.back-reference.regexp", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "(?", + "t": "string.regexp.ts punctuation.definition.group.assertion.regexp meta.assertion.look-ahead.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "!", + "t": "string.regexp.ts keyword.operator.regexp punctuation.definition.group.assertion.regexp punctuation.definition.group.assertion.regexp", + "r": { + "dark_plus": "punctuation.definition.group.assertion.regexp: #CE9178", + "light_plus": "punctuation.definition.group.assertion.regexp: #D16969", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "punctuation.definition.group.assertion.regexp: #CE9178", + "hc_light": "punctuation.definition.group.assertion.regexp: #D16969", + "light_modern": "punctuation.definition.group.assertion.regexp: #D16969" + } + }, + { + "c": "\\s", + "t": "string.regexp.ts constant.character.escape.regexp", + "r": { + "dark_plus": "constant.character.escape: #D7BA7D", + "light_plus": "constant.character.escape: #EE0000", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "constant.character: #569CD6", + "dark_modern": "constant.character.escape: #D7BA7D", + "hc_light": "constant.character.escape: #EE0000", + "light_modern": "constant.character.escape: #EE0000" + } + }, + { + "c": ")", + "t": "string.regexp.ts punctuation.definition.group.regexp", + "r": { + "dark_plus": "punctuation.definition.group.regexp: #CE9178", + "light_plus": "punctuation.definition.group.regexp: #D16969", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "punctuation.definition.group.regexp: #CE9178", + "hc_light": "punctuation.definition.group.regexp: #D16969", + "light_modern": "punctuation.definition.group.regexp: #D16969" + } + }, + { + "c": "/", + "t": "string.regexp.ts", + "r": { + "dark_plus": "string.regexp: #D16969", + "light_plus": "string.regexp: #811F3F", + "dark_vs": "string.regexp: #D16969", + "light_vs": "string.regexp: #811F3F", + "hc_black": "string.regexp: #D16969", + "dark_modern": "string.regexp: #D16969", + "hc_light": "string.regexp: #811F3F", + "light_modern": "string.regexp: #811F3F" + } + }, + { + "c": "u", + "t": "string.regexp.ts keyword.ts", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, { "c": ";", "t": "punctuation.delimiter.ts", diff --git a/code/extensions/vscode-test-resolver/package-lock.json b/code/extensions/vscode-test-resolver/package-lock.json index 367c4dca2e0..3bdbeac9b3e 100644 --- a/code/extensions/vscode-test-resolver/package-lock.json +++ b/code/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/code/extensions/yaml/package.json b/code/extensions/yaml/package.json index 486458e8ad6..66f0a200d08 100644 --- a/code/extensions/yaml/package.json +++ b/code/extensions/yaml/package.json @@ -95,7 +95,10 @@ "editor.tabSize": 2, "editor.autoIndent": "advanced", "diffEditor.ignoreTrimWhitespace": false, - "editor.defaultColorDecorators": "never" + "editor.defaultColorDecorators": "never", + "editor.quickSuggestions": { + "strings": "on" + } }, "[dockercompose]": { "editor.insertSpaces": true, diff --git a/code/package-lock.json b/code/package-lock.json index 5fd6ec1be6a..3d641665e24 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -1,23 +1,22 @@ { "name": "che-code", - "version": "1.99.3", + "version": "1.100.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "che-code", - "version": "1.99.3", + "version": "1.100.3", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", "@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.2", "@vscode/proxy-agent": "^0.32.0", "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", @@ -28,16 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "js-yaml": "^4.1.0", @@ -62,7 +61,7 @@ "che-code": "out/vs/server/main.js" }, "devDependencies": { - "@playwright/test": "1.46.1", + "@playwright/test": "^1.50.0", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", @@ -103,7 +102,7 @@ "css-loader": "^6.9.1", "cssnano": "^6.0.3", "debounce": "^1.0.0", - "deemon": "^1.8.0", + "deemon": "^1.13.4", "electron": "30.5.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -162,7 +161,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "5.6.0-dev.20240715", + "typescript": "5.9.0-dev.20250416", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -221,29 +220,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": { @@ -280,19 +281,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-lro": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.1.tgz", @@ -416,80 +404,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", @@ -684,19 +612,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" } @@ -711,100 +641,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==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "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==", + "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/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.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==", + "node_modules/@babel/parser": { + "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": { - "has-flag": "^3.0.0" + "@babel/types": "^7.27.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==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -813,14 +671,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" @@ -857,14 +716,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" @@ -876,18 +735,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@c4312/eventsource-umd": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", - "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1115,17 +962,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", @@ -1586,18 +1448,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" @@ -1663,12 +1527,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": { @@ -2024,46 +1889,17 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", - "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", - "dev": true, - "dependencies": { - "playwright": "1.46.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", - "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.46.1" + "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.46.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", - "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, "engines": { "node": ">=18" } @@ -2411,12 +2247,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": { @@ -2844,9 +2681,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.2", + "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.3.2.tgz", + "integrity": "sha512-fmNPYysU2ioH99uCaBPiRblEZSnir5cTmc7w91hAxAoYoGpHt2PZPxT5eIOn7FGmPOsjLdQcd6fduFJGYVD4Mw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3456,30 +3293,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3489,64 +3326,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", - "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", + "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -4171,13 +4008,15 @@ "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": "4.1.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", - "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", + "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.5.4", @@ -4201,6 +4040,7 @@ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", "dev": true, + "license": "Apache-2.0", "optional": true, "engines": { "bare": ">=1.14.0" @@ -4211,6 +4051,7 @@ "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": "^3.0.1" @@ -4221,6 +4062,7 @@ "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.21.0" @@ -5734,10 +5576,11 @@ } }, "node_modules/deemon": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.8.0.tgz", - "integrity": "sha512-qcuSMls/W5DdoEKKAF0PiNQrc8+tItFjvszfjNm1YqNv1p5wwEt+6qILA9sws6eM81nmNwD38ducqlgIXzQlsQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.4.tgz", + "integrity": "sha512-O+7MRrNEddXeZXJusSSkFfBsJ5faVt+XpjfIosciuaK6StTjMi5Q4poYUxFlrIPHusrhrc4isC1gxt9nLijf6Q==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^4.0.2", "tree-kill": "^1.2.2" @@ -6105,10 +5948,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", @@ -6726,15 +6570,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -10948,7 +10783,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", @@ -11146,9 +10982,9 @@ } }, "node_modules/koa": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.4.tgz", - "integrity": "sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "dev": true, "license": "MIT", "dependencies": { @@ -11686,8 +11522,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", @@ -11761,8 +11598,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", @@ -14516,6 +14354,7 @@ "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", @@ -16361,6 +16200,7 @@ "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", "text-decoder": "^1.1.0" @@ -16775,6 +16615,7 @@ "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" @@ -17067,15 +16908,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", @@ -17444,9 +17276,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.6.0-dev.20240715", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.0-dev.20240715.tgz", - "integrity": "sha512-CLF8WFoqLgHgxQqjklkEOw3gT99Y2YNU4+TfkJurX5bfejAUYpb2jBjiYOn5Rq9HCew6ceZlRaG7Q++6/fBvVA==", + "version": "5.9.0-dev.20250416", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250416.tgz", + "integrity": "sha512-NCJUSWqeGImKoCTOydww1MhHvtNjU5GmhJ5LAhqjvZOYcHJSO0DfYSKb39klxfwZoHS9aNgay9bVIWbfODpCUA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -17628,10 +17460,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/code/package.json b/code/package.json index eb5e594b1a8..885104b0258 100644 --- a/code/package.json +++ b/code/package.json @@ -1,7 +1,7 @@ { "name": "che-code", - "version": "1.99.3", - "distro": "21c8d8ea1e46d97c5639a7cabda6c0e063cc8dd5", + "version": "1.100.3", + "distro": "d816c925ee695768ef33998a28d20477316e4c25", "author": { "name": "Microsoft Corporation" }, @@ -69,14 +69,13 @@ "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" }, "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", "@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.2", "@vscode/proxy-agent": "^0.32.0", "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", @@ -87,16 +86,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -118,7 +117,7 @@ "js-yaml": "^4.1.0" }, "devDependencies": { - "@playwright/test": "1.46.1", + "@playwright/test": "^1.50.0", "@stylistic/eslint-plugin-ts": "^2.8.0", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", @@ -157,7 +156,7 @@ "css-loader": "^6.9.1", "cssnano": "^6.0.3", "debounce": "^1.0.0", - "deemon": "^1.8.0", + "deemon": "^1.13.4", "electron": "30.5.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -216,7 +215,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "5.6.0-dev.20240715", + "typescript": "5.9.0-dev.20250416", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -240,12 +239,6 @@ }, "kerberos@2.1.1": { "node-addon-api": "7.1.0" - }, - "@vscode/test-web": { - "tar-fs": "3.0.8" - }, - "prebuild-install": { - "tar-fs": "2.1.2" } }, "repository": { diff --git a/code/product.json b/code/product.json index 0eee21d59ba..e00286fe13d 100644 --- a/code/product.json +++ b/code/product.json @@ -53,8 +53,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.97.1", - "sha256": "977dd854805547702e312e176f68a1b142fa123f228258f47f0964560ad32496", + "version": "1.100.1", + "sha256": "8c2218df3422d45b95e96d9d28cdc4aa4426a2799aaaedd862d3f60ecab03844", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/code/remote/.npmrc b/code/remote/.npmrc index e2c53927b15..3f17dea3fd9 100644 --- a/code/remote/.npmrc +++ b/code/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="20.18.3" -ms_build_id="323695" +target="20.19.0" +ms_build_id="332907" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/code/remote/package-lock.json b/code/remote/package-lock.json index 8055252bafd..272dca8d3b7 100644 --- a/code/remote/package-lock.json +++ b/code/remote/package-lock.json @@ -8,7 +8,6 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@kubernetes/client-node": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", @@ -22,16 +21,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -50,23 +49,10 @@ "yazl": "^2.4.3" } }, - "node_modules/@c4312/eventsource-umd": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", - "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -83,7 +69,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -95,7 +80,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "license": "MIT", "engines": { "node": ">= 10.16.0" }, @@ -107,7 +91,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "license": "MIT", "engines": { "node": ">= 10.16.0" }, @@ -137,9 +120,9 @@ } }, "node_modules/@kubernetes/client-node/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -644,30 +627,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -677,64 +660,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", - "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", + "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/agent-base": { @@ -768,7 +751,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -780,7 +762,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -959,7 +940,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -970,8 +950,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1084,8 +1063,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -1106,8 +1084,7 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -1117,15 +1094,6 @@ "once": "^1.4.0" } }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1432,7 +1400,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", "engines": { "node": ">=8" } @@ -2266,7 +2233,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -2284,7 +2250,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2298,7 +2263,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", "engines": { "node": ">=8" } @@ -2306,14 +2270,12 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2325,7 +2287,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2341,7 +2302,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2353,7 +2313,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", "engines": { "node": ">=8" } @@ -2387,6 +2346,7 @@ "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", @@ -2598,7 +2558,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2616,7 +2575,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2633,7 +2591,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", "engines": { "node": ">=8" } @@ -2642,7 +2599,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2656,14 +2612,12 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2677,7 +2631,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, diff --git a/code/remote/package.json b/code/remote/package.json index f575afee815..6a37a7494a6 100644 --- a/code/remote/package.json +++ b/code/remote/package.json @@ -4,7 +4,6 @@ "private": true, "dependencies": { "@kubernetes/client-node": "^0.22.0", - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -17,16 +16,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -48,9 +47,6 @@ "node-gyp-build": "4.8.1", "kerberos@2.1.1": { "node-addon-api": "7.1.0" - }, - "prebuild-install": { - "tar-fs": "2.1.2" } } } diff --git a/code/remote/web/package-lock.json b/code/remote/web/package-lock.json index 63910bd2bb6..a6c826c2a73 100644 --- a/code/remote/web/package-lock.json +++ b/code/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -90,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -123,58 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/code/remote/web/package.json b/code/remote/web/package.json index 1ca215cc76e..167d8e4dbba 100644 --- a/code/remote/web/package.json +++ b/code/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/code/resources/linux/snap/snapcraft.yaml b/code/resources/linux/snap/snapcraft.yaml index 1d7412bdc71..6f4962af70a 100644 --- a/code/resources/linux/snap/snapcraft.yaml +++ b/code/resources/linux/snap/snapcraft.yaml @@ -61,6 +61,7 @@ parts: override-build: | snapcraftctl build patchelf --force-rpath --set-rpath '$ORIGIN/../../lib/x86_64-linux-gnu:$ORIGIN:/snap/core20/current/lib/x86_64-linux-gnu' $SNAPCRAFT_PART_INSTALL/usr/share/@@NAME@@/chrome_crashpad_handler + chmod 0755 $SNAPCRAFT_PART_INSTALL/usr/share/@@NAME@@/chrome-sandbox cleanup: after: - code diff --git a/code/scripts/test-integration.sh b/code/scripts/test-integration.sh index 89006480308..33d00615359 100755 --- a/code/scripts/test-integration.sh +++ b/code/scripts/test-integration.sh @@ -6,9 +6,6 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - # --disable-dev-shm-usage: when run on docker containers where size of /dev/shm - # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory - LINUX_EXTRA_ARGS="--disable-dev-shm-usage" fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` @@ -55,13 +52,13 @@ fi echo echo "### API tests (folder)" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests $API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests $API_TESTS_EXTRA_ARGS kill_app echo echo "### API tests (workspace)" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests $API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests $API_TESTS_EXTRA_ARGS kill_app echo @@ -71,7 +68,7 @@ npm run test-extension -- -l vscode-colorize-tests kill_app echo -echo "### Terminal Suggest tests" +echo "### Terminal Suggest tests" echo npm run test-extension -- -l terminal-suggest --enable-proposed-api=vscode.vscode-api-tests kill_app @@ -79,7 +76,7 @@ kill_app echo echo "### TypeScript tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit $API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit $API_TESTS_EXTRA_ARGS kill_app echo @@ -91,13 +88,13 @@ kill_app echo echo "### Emmet tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test $API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test $API_TESTS_EXTRA_ARGS kill_app echo echo "### Git tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test $API_TESTS_EXTRA_ARGS +"$INTEGRATION_TEST_ELECTRON_PATH" $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test $API_TESTS_EXTRA_ARGS kill_app echo diff --git a/code/scripts/test-remote-integration.sh b/code/scripts/test-remote-integration.sh index 4695dfa12e3..7325757418e 100755 --- a/code/scripts/test-remote-integration.sh +++ b/code/scripts/test-remote-integration.sh @@ -6,9 +6,6 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - # --disable-dev-shm-usage: when run on docker containers where size of /dev/shm - # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory - LINUX_EXTRA_ARGS="--disable-dev-shm-usage" fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` @@ -79,49 +76,49 @@ echo "Storing log files into '$VSCODELOGSDIR'." echo echo "### API tests (folder)" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/singlefolder-tests $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$REMOTE_VSCODE/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/singlefolder-tests $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### API tests (workspace)" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --file-uri=$REMOTE_VSCODE/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/workspace-tests $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --file-uri=$REMOTE_VSCODE/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/workspace-tests $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### TypeScript tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/typescript-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/typescript-language-features --extensionTestsPath=$REMOTE_VSCODE/typescript-language-features/out/test/unit $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$REMOTE_VSCODE/typescript-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/typescript-language-features --extensionTestsPath=$REMOTE_VSCODE/typescript-language-features/out/test/unit $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### Markdown tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/markdown-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/markdown-language-features --extensionTestsPath=$REMOTE_VSCODE/markdown-language-features/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$REMOTE_VSCODE/markdown-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/markdown-language-features --extensionTestsPath=$REMOTE_VSCODE/markdown-language-features/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### Emmet tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/emmet/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/emmet --extensionTestsPath=$REMOTE_VSCODE/emmet/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$REMOTE_VSCODE/emmet/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/emmet --extensionTestsPath=$REMOTE_VSCODE/emmet/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### Git tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/git --extensionTestsPath=$REMOTE_VSCODE/git/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/git --extensionTestsPath=$REMOTE_VSCODE/git/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### Ipynb tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/ipynb --extensionTestsPath=$REMOTE_VSCODE/ipynb/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/ipynb --extensionTestsPath=$REMOTE_VSCODE/ipynb/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app echo echo "### Configuration editing tests" echo -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/configuration-editing --extensionTestsPath=$REMOTE_VSCODE/configuration-editing/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +"$INTEGRATION_TEST_ELECTRON_PATH" --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/configuration-editing --extensionTestsPath=$REMOTE_VSCODE/configuration-editing/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app # Cleanup diff --git a/code/scripts/test.sh b/code/scripts/test.sh index ae0d88cc734..9ba8dedee0f 100755 --- a/code/scripts/test.sh +++ b/code/scripts/test.sh @@ -6,9 +6,6 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - # --disable-dev-shm-usage: when run on docker containers where size of /dev/shm - # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory - LINUX_EXTRA_ARGS="--disable-dev-shm-usage" fi cd $ROOT @@ -39,5 +36,5 @@ else cd $ROOT ; \ ELECTRON_ENABLE_LOGGING=1 \ "$CODE" \ - test/unit/electron/index.js --crash-reporter-directory=$VSCODECRASHDIR $LINUX_EXTRA_ARGS "$@" + test/unit/electron/index.js --crash-reporter-directory=$VSCODECRASHDIR "$@" fi diff --git a/code/src/tsconfig.base.json b/code/src/tsconfig.base.json index e354b0ed463..6d276949648 100644 --- a/code/src/tsconfig.base.json +++ b/code/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/code/src/tsconfig.monaco.json b/code/src/tsconfig.monaco.json index d64d6bd8c8e..cad1e06383d 100644 --- a/code/src/tsconfig.monaco.json +++ b/code/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/code/src/vscode-dts/vscode.proposed.languageModelToolsForAgent.d.ts b/code/src/typings/css.d.ts similarity index 76% rename from code/src/vscode-dts/vscode.proposed.languageModelToolsForAgent.d.ts rename to code/src/typings/css.d.ts index 9190a1c18b8..abb66921781 100644 --- a/code/src/vscode-dts/vscode.proposed.languageModelToolsForAgent.d.ts +++ b/code/src/typings/css.d.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare module 'vscode' { - // Enables access to providing language model tools to agent mode -} + +// Recognize all CSS files as valid module imports +declare module "vs/css!*" { } +declare module "*.css" { } diff --git a/code/src/vs/base/browser/dom.ts b/code/src/vs/base/browser/dom.ts index 670525a0109..7227aba24f3 100644 --- a/code/src/vs/base/browser/dom.ts +++ b/code/src/vs/base/browser/dom.ts @@ -696,6 +696,20 @@ export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePositi }; } +/** + * Returns whether the element is in the bottom right quarter of the container. + * + * @param element the element to check for being in the bottom right quarter + * @param container the container to check against + * @returns true if the element is in the bottom right quarter of the container + */ +export function isElementInBottomRightQuarter(element: HTMLElement, container: HTMLElement): boolean { + const position = getDomNodePagePosition(element); + const clientArea = getClientArea(container); + + return position.left > clientArea.width / 2 && position.top > clientArea.height / 2; +} + /** * Returns the effective zoom on a given element before window zoom level is applied */ diff --git a/code/src/vs/base/browser/mouseEvent.ts b/code/src/vs/base/browser/mouseEvent.ts index 1e6f8b3032a..3cad09d7dcc 100644 --- a/code/src/vs/base/browser/mouseEvent.ts +++ b/code/src/vs/base/browser/mouseEvent.ts @@ -22,6 +22,7 @@ export interface IMouseEvent { readonly altKey: boolean; readonly metaKey: boolean; readonly timestamp: number; + readonly defaultPrevented: boolean; preventDefault(): void; stopPropagation(): void; @@ -44,6 +45,7 @@ export class StandardMouseEvent implements IMouseEvent { public readonly altKey: boolean; public readonly metaKey: boolean; public readonly timestamp: number; + public readonly defaultPrevented: boolean; constructor(targetWindow: Window, e: MouseEvent) { this.timestamp = Date.now(); @@ -52,6 +54,7 @@ export class StandardMouseEvent implements IMouseEvent { this.middleButton = e.button === 1; this.rightButton = e.button === 2; this.buttons = e.buttons; + this.defaultPrevented = e.defaultPrevented; this.target = e.target; diff --git a/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 872f328858f..d2ce6023d87 100644 Binary files a/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/code/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/code/src/vs/base/browser/ui/dropdown/dropdown.ts b/code/src/vs/base/browser/ui/dropdown/dropdown.ts index b4a7d38a6fb..4bb319918f3 100644 --- a/code/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/code/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -22,12 +22,12 @@ export interface ILabelRenderer { (container: HTMLElement): IDisposable | null; } -interface IBaseDropdownOptions { +export interface IBaseDropdownOptions { label?: string; labelRenderer?: ILabelRenderer; } -class BaseDropdown extends ActionRunner { +export class BaseDropdown extends ActionRunner { private _element: HTMLElement; private boxContainer?: HTMLElement; private _label?: HTMLElement; diff --git a/code/src/vs/base/browser/ui/hover/hover.ts b/code/src/vs/base/browser/ui/hover/hover.ts index 6b877ec17fd..6c682c3c083 100644 --- a/code/src/vs/base/browser/ui/hover/hover.ts +++ b/code/src/vs/base/browser/ui/hover/hover.ts @@ -325,6 +325,13 @@ export interface IHoverAppearanceOptions { * another in the same group so it looks like the hover is moving from one element to the other. */ skipFadeInAnimation?: boolean; + + /** + * The max height of the hover relative to the window height. + * Accepted values: (0,1] + * Default: 0.5 + */ + maxHeightRatio?: number; } export interface IHoverAction { diff --git a/code/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/code/src/vs/base/browser/ui/hover/hoverDelegate2.ts index b49cb84951c..0f57de0379a 100644 --- a/code/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/code/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -13,7 +13,12 @@ let baseHoverDelegate: IHoverDelegate2 = { setupDelayedHoverAtMouse: () => Disposable.None, hideHover: () => undefined, showAndFocusLastHover: () => undefined, - setupManagedHover: () => null!, + setupManagedHover: () => ({ + dispose: () => undefined, + show: () => undefined, + hide: () => undefined, + update: () => undefined, + }), showManagedHover: () => undefined }; diff --git a/code/src/vs/base/browser/ui/list/listPaging.ts b/code/src/vs/base/browser/ui/list/listPaging.ts index e2608993f70..bb01170201c 100644 --- a/code/src/vs/base/browser/ui/list/listPaging.ts +++ b/code/src/vs/base/browser/ui/list/listPaging.ts @@ -110,6 +110,7 @@ export interface IPagedListOptions { readonly horizontalScrolling?: boolean; readonly scrollByPage?: boolean; readonly paddingBottom?: number; + readonly alwaysConsumeMouseWheel?: boolean; } function fromPagedListOptions(modelProvider: () => IPagedModel, options: IPagedListOptions): IListOptions { diff --git a/code/src/vs/base/browser/ui/severityIcon/media/severityIcon.css b/code/src/vs/base/browser/ui/severityIcon/media/severityIcon.css index 62d99edf337..0e0a140a89c 100644 --- a/code/src/vs/base/browser/ui/severityIcon/media/severityIcon.css +++ b/code/src/vs/base/browser/ui/severityIcon/media/severityIcon.css @@ -8,7 +8,6 @@ .text-search-provider-messages .providerMessage .codicon.codicon-error, .extensions-viewlet > .extensions .codicon.codicon-error, .extension-editor .codicon.codicon-error, -.preferences-editor .codicon.codicon-error, .chat-attached-context-attachment .codicon.codicon-error { color: var(--vscode-problemsErrorIcon-foreground); } @@ -26,7 +25,6 @@ .markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-info, .text-search-provider-messages .providerMessage .codicon.codicon-info, .extensions-viewlet > .extensions .codicon.codicon-info, -.extension-editor .codicon.codicon-info, -.preferences-editor .codicon.codicon-info { +.extension-editor .codicon.codicon-info { color: var(--vscode-problemsInfoIcon-foreground); } diff --git a/code/src/vs/base/browser/ui/toggle/toggle.ts b/code/src/vs/base/browser/ui/toggle/toggle.ts index a6c913662f9..b4d6e145634 100644 --- a/code/src/vs/base/browser/ui/toggle/toggle.ts +++ b/code/src/vs/base/browser/ui/toggle/toggle.ts @@ -37,6 +37,9 @@ export interface ICheckboxStyles { readonly checkboxBackground: string | undefined; readonly checkboxBorder: string | undefined; readonly checkboxForeground: string | undefined; + readonly checkboxDisabledBackground: string | undefined; + readonly checkboxDisabledForeground: string | undefined; + readonly size?: number; } export const unthemedToggleStyles = { @@ -156,6 +159,10 @@ export class Toggle extends Widget { this._register(this.ignoreGesture(this.domNode)); this.onkeydown(this.domNode, (keyboardEvent) => { + if (!this.enabled) { + return; + } + if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) { this.checked = !this._checked; this._onChange.fire(true); @@ -286,16 +293,28 @@ export class Checkbox extends Widget { enable(): void { this.checkbox.enable(); + this.applyStyles(true); } disable(): void { this.checkbox.disable(); + this.applyStyles(false); } - protected applyStyles(): void { - this.domNode.style.color = this.styles.checkboxForeground || ''; - this.domNode.style.backgroundColor = this.styles.checkboxBackground || ''; - this.domNode.style.borderColor = this.styles.checkboxBorder || ''; + setTitle(newTitle: string): void { + this.checkbox.setTitle(newTitle); + } + + protected applyStyles(enabled = this.enabled): void { + this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || ''; + this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || ''; + this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || ''; + + const size = this.styles.size || 18; + this.domNode.style.width = + this.domNode.style.height = + this.domNode.style.fontSize = `${size}px`; + this.domNode.style.fontSize = `${size - 2}px`; } } diff --git a/code/src/vs/base/common/arrays.ts b/code/src/vs/base/common/arrays.ts index 98bf168f39d..7efd1f97fc9 100644 --- a/code/src/vs/base/common/arrays.ts +++ b/code/src/vs/base/common/arrays.ts @@ -193,6 +193,10 @@ export function forEachWithNeighbors(arr: T[], f: (before: T | undefined, ele } } +export function concatArrays(...arrays: TArr): TArr[number][number][] { + return ([] as any[]).concat(...arrays); +} + interface IMutableSplice extends ISplice { readonly toInsert: T[]; deleteCount: number; @@ -731,13 +735,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/code/src/vs/base/common/async.ts b/code/src/vs/base/common/async.ts index bd772e5d2e9..a53c2c490c9 100644 --- a/code/src/vs/base/common/async.ts +++ b/code/src/vs/base/common/async.ts @@ -6,7 +6,7 @@ import { CancellationToken, CancellationTokenSource } from './cancellation.js'; import { BugIndicatingError, CancellationError } from './errors.js'; import { Emitter, Event } from './event.js'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from './lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, MutableDisposable, toDisposable } from './lifecycle.js'; import { extUri as defaultExtUri, IExtUri } from './resources.js'; import { URI } from './uri.js'; import { setTimeout0 } from './platform.js'; @@ -21,19 +21,41 @@ export interface CancelablePromise extends Promise { cancel(): void; } +/** + * Returns a promise that can be cancelled using the provided cancellation token. + * + * @remarks When cancellation is requested, the promise will be rejected with a {@link CancellationError}. + * If the promise resolves to a disposable object, it will be automatically disposed when cancellation + * is requested. + * + * @param callback A function that accepts a cancellation token and returns a promise + * @returns A promise that can be cancelled + */ export function createCancelablePromise(callback: (token: CancellationToken) => Promise): CancelablePromise { const source = new CancellationTokenSource(); const thenable = callback(source.token); + + let isCancelled = false; + const promise = new Promise((resolve, reject) => { const subscription = source.token.onCancellationRequested(() => { + isCancelled = true; subscription.dispose(); reject(new CancellationError()); }); Promise.resolve(thenable).then(value => { subscription.dispose(); source.dispose(); - resolve(value); + + if (!isCancelled) { + resolve(value); + + } else if (isDisposable(value)) { + // promise has been cancelled, result is disposable and will + // be cleaned up + value.dispose(); + } }, err => { subscription.dispose(); source.dispose(); @@ -1303,6 +1325,7 @@ export class ThrottledWorker extends Disposable { override dispose(): void { super.dispose(); + this.pendingWork.length = 0; this.disposed = true; } } diff --git a/code/src/vs/base/common/codecs/baseDecoder.ts b/code/src/vs/base/common/codecs/baseDecoder.ts index e483b260d37..442f67817be 100644 --- a/code/src/vs/base/common/codecs/baseDecoder.ts +++ b/code/src/vs/base/common/codecs/baseDecoder.ts @@ -5,10 +5,10 @@ import { assert } from '../assert.js'; import { Emitter } from '../event.js'; -import { IDisposable } from '../lifecycle.js'; import { ReadableStream } from '../stream.js'; import { DeferredPromise } from '../async.js'; import { AsyncDecoder } from './asyncDecoder.js'; +import { DisposableMap, IDisposable } from '../lifecycle.js'; import { ObservableDisposable } from '../observableDisposable.js'; /** @@ -38,7 +38,10 @@ export abstract class BaseDecoder< /** * A store of currently registered event listeners. */ - private readonly _listeners: Map> = new Map(); + private readonly _listeners: DisposableMap< + TStreamListenerNames, + DisposableMap + > = this._register(new DisposableMap()); /** * This method is called when a new incoming data @@ -116,9 +119,15 @@ export abstract class BaseDecoder< } this.started = true; - this.stream.on('data', this.tryOnStreamData); - this.stream.on('error', this.onStreamError); + /** + * !NOTE! The order of event subscriptions is critical here because + * the `data` event is also starts the stream, hence changing + * the order of event subscriptions can lead to race conditions. + * See {@link ReadableStreamEvents} for more info. + */ this.stream.on('end', this.onStreamEnd); + this.stream.on('error', this.onStreamError); + this.stream.on('data', this.tryOnStreamData); // this allows to compose decoders together, - if a decoder // instance is passed as a readable stream to this decoder, @@ -181,7 +190,7 @@ export abstract class BaseDecoder< let currentListeners = this._listeners.get('data'); if (!currentListeners) { - currentListeners = new Map(); + currentListeners = new DisposableMap(); this._listeners.set('data', currentListeners); } @@ -201,7 +210,7 @@ export abstract class BaseDecoder< let currentListeners = this._listeners.get('error'); if (!currentListeners) { - currentListeners = new Map(); + currentListeners = new DisposableMap(); this._listeners.set('error', currentListeners); } @@ -221,32 +230,13 @@ export abstract class BaseDecoder< let currentListeners = this._listeners.get('end'); if (!currentListeners) { - currentListeners = new Map(); + currentListeners = new DisposableMap(); this._listeners.set('end', currentListeners); } currentListeners.set(callback, this._onEnd.event(callback)); } - /** - * Remove all existing event listeners. - */ - public removeAllListeners(): void { - // remove listeners set up by this class - this.stream.removeListener('data', this.tryOnStreamData); - this.stream.removeListener('error', this.onStreamError); - this.stream.removeListener('end', this.onStreamEnd); - - // remove listeners set up by external consumers - for (const [name, listeners] of this._listeners.entries()) { - this._listeners.delete(name); - for (const [listener, disposable] of listeners) { - disposable.dispose(); - listeners.delete(listener); - } - } - } - /** * Pauses the stream. */ @@ -275,7 +265,7 @@ export abstract class BaseDecoder< } /** - * Removes a priorly-registered event listener for a specified event. + * Removes a previously-registered event listener for a specified event. * * Note! * - the callback function must be the same as the one that was used when @@ -283,22 +273,20 @@ export abstract class BaseDecoder< * remove the listener * - this method is idempotent and results in no-op if the listener is * not found, therefore passing incorrect `callback` function may - * result in silent unexpected behaviour + * result in silent unexpected behavior */ - public removeListener(event: string, callback: Function): void { - for (const [nameName, listeners] of this._listeners.entries()) { - if (nameName !== event) { + public removeListener(eventName: TStreamListenerNames, callback: Function): void { + const listeners = this._listeners.get(eventName); + if (listeners === undefined) { + return; + } + + for (const [listener] of listeners) { + if (listener !== callback) { continue; } - for (const [listener, disposable] of listeners) { - if (listener !== callback) { - continue; - } - - disposable.dispose(); - listeners.delete(listener); - } + listeners.deleteAndDispose(listener); } } @@ -363,14 +351,15 @@ export abstract class BaseDecoder< } public override dispose(): void { - if (this.disposed) { - return; - } + this.settledPromise.complete(); - this.onStreamEnd(); + // remove all existing event listeners + this._listeners.clearAndDisposeAll(); + this.stream.removeListener('data', this.tryOnStreamData); + this.stream.removeListener('error', this.onStreamError); + this.stream.removeListener('end', this.onStreamEnd); this.stream.destroy(); - this.removeAllListeners(); super.dispose(); } } diff --git a/code/src/vs/base/common/codiconsLibrary.ts b/code/src/vs/base/common/codiconsLibrary.ts index 13a3a4f0c31..d6ce926199b 100644 --- a/code/src/vs/base/common/codiconsLibrary.ts +++ b/code/src/vs/base/common/codiconsLibrary.ts @@ -592,4 +592,9 @@ export const codiconsLibrary = { flag: register('flag', 0xec3f), lightbulbEmpty: register('lightbulb-empty', 0xec40), symbolMethodArrow: register('symbol-method-arrow', 0xec41), + copilotUnavailable: register('copilot-unavailable', 0xec42), + repoPinned: register('repo-pinned', 0xec43), + keyboardTabAbove: register('keyboard-tab-above', 0xec44), + keyboardTabBelow: register('keyboard-tab-below', 0xec45), + gitPullRequestDone: register('git-pull-request-done', 0xec46), } as const; diff --git a/code/src/vs/base/common/color.ts b/code/src/vs/base/common/color.ts index 9f20f7080ab..3263e2d9922 100644 --- a/code/src/vs/base/common/color.ts +++ b/code/src/vs/base/common/color.ts @@ -494,6 +494,25 @@ export class Color { return new Color(new RGBA(r, g, b, a)); } + /** + * Mixes the current color with the provided color based on the given factor. + * @param color The color to mix with + * @param factor The factor of mixing (0 means this color, 1 means the input color, 0.5 means equal mix) + * @returns A new color representing the mix + */ + mix(color: Color, factor: number = 0.5): Color { + const normalize = Math.min(Math.max(factor, 0), 1); + const thisRGBA = this.rgba; + const otherRGBA = color.rgba; + + const r = thisRGBA.r + (otherRGBA.r - thisRGBA.r) * normalize; + const g = thisRGBA.g + (otherRGBA.g - thisRGBA.g) * normalize; + const b = thisRGBA.b + (otherRGBA.b - thisRGBA.b) * normalize; + const a = thisRGBA.a + (otherRGBA.a - thisRGBA.a) * normalize; + + return new Color(new RGBA(r, g, b, a)); + } + makeOpaque(opaqueBackground: Color): Color { if (this.isOpaque() || opaqueBackground.rgba.a !== 1) { // only allow to blend onto a non-opaque color onto a opaque color diff --git a/code/src/vs/base/common/glob.ts b/code/src/vs/base/common/glob.ts index 79fc4fe96dd..1057364d7fc 100644 --- a/code/src/vs/base/common/glob.ts +++ b/code/src/vs/base/common/glob.ts @@ -305,6 +305,26 @@ const NULL = function (): string | null { return null; }; +/** + * Check if a provided parsed pattern or expression + * is empty - hence it won't ever match anything. + * + * See {@link FALSE} and {@link NULL}. + */ +export const isEmptyPattern = ( + pattern: ParsedPattern | ParsedExpression, +): pattern is (typeof FALSE | typeof NULL) => { + if (pattern === FALSE) { + return true; + } + + if (pattern === NULL) { + return true; + } + + return false; +}; + function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern { if (!arg1) { return NULL; diff --git a/code/src/vs/base/common/marshallingIds.ts b/code/src/vs/base/common/marshallingIds.ts index c83402b47c5..9ea80910eeb 100644 --- a/code/src/vs/base/common/marshallingIds.ts +++ b/code/src/vs/base/common/marshallingIds.ts @@ -26,5 +26,6 @@ export const enum MarshalledId { LanguageModelToolResult, LanguageModelTextPart, LanguageModelPromptTsxPart, - LanguageModelDataPart + LanguageModelDataPart, + LanguageModelExtraDataPart, } diff --git a/code/src/vs/base/common/observableDisposable.ts b/code/src/vs/base/common/observableDisposable.ts index 3bce45ffa5a..73808329ed3 100644 --- a/code/src/vs/base/common/observableDisposable.ts +++ b/code/src/vs/base/common/observableDisposable.ts @@ -37,12 +37,12 @@ export abstract class ObservableDisposable extends Disposable { } /** - * Tracks disposed state of this object. + * Tracks 'disposed' state of this object. */ private _disposed = false; /** - * Check if the current object was already disposed. + * Gets current 'disposed' state of this object. */ public get disposed(): boolean { return this._disposed; @@ -56,8 +56,8 @@ export abstract class ObservableDisposable extends Disposable { if (this.disposed) { return; } - this._disposed = true; + this._onDispose.fire(); super.dispose(); } diff --git a/code/src/vs/base/common/observableInternal/autorun.ts b/code/src/vs/base/common/observableInternal/autorun.ts index 03c37aa6c19..25055082928 100644 --- a/code/src/vs/base/common/observableInternal/autorun.ts +++ b/code/src/vs/base/common/observableInternal/autorun.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChangeContext, IObservable, IObservableWithChange, IObserver, IReader } from './base.js'; +import { IObservable, IObservableWithChange, IObserver, IReader } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; import { getLogger } from './logging/logging.js'; +import { IChangeTracker } from './changeTracker.js'; /** * Runs immediately and whenever a transaction ends and an observed observable changed. @@ -16,7 +17,6 @@ export function autorun(fn: (reader: IReader) => void): IDisposable { return new AutorunObserver( new DebugNameData(undefined, undefined, fn), fn, - undefined, undefined ); } @@ -29,7 +29,6 @@ export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) return new AutorunObserver( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, - undefined, undefined ); } @@ -38,8 +37,8 @@ export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) * Runs immediately and whenever a transaction ends and an observed observable changed. * {@link fn} should start with a JS Doc using `@description` to name the autorun. * - * Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes. - * Use `handleChange` to add a reported change to the change summary. + * Use `changeTracker.createChangeSummary` to create a "change summary" that can collect the changes. + * Use `changeTracker.handleChange` to add a reported change to the change summary. * The run function is given the last change summary. * The change summary is discarded after the run function was called. * @@ -47,16 +46,14 @@ export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) */ export function autorunHandleChanges( options: IDebugNameData & { - createEmptyChangeSummary?: () => TChangeSummary; - handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean; + changeTracker: IChangeTracker; }, fn: (reader: IReader, changeSummary: TChangeSummary) => void ): IDisposable { return new AutorunObserver( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, - options.createEmptyChangeSummary, - options.handleChange + options.changeTracker, ); } @@ -65,8 +62,7 @@ export function autorunHandleChanges( */ export function autorunWithStoreHandleChanges( options: IDebugNameData & { - createEmptyChangeSummary?: () => TChangeSummary; - handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean; + changeTracker: IChangeTracker; }, fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void ): IDisposable { @@ -76,8 +72,7 @@ export function autorunWithStoreHandleChanges( owner: options.owner, debugName: options.debugName, debugReferenceFn: options.debugReferenceFn ?? fn, - createEmptyChangeSummary: options.createEmptyChangeSummary, - handleChange: options.handleChange, + changeTracker: options.changeTracker, }, (reader, changeSummary) => { store.clear(); @@ -183,10 +178,9 @@ export class AutorunObserver implements IObserver, IReader constructor( public readonly _debugNameData: DebugNameData, public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void, - private readonly createChangeSummary: (() => TChangeSummary) | undefined, - private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, + private readonly _changeTracker: IChangeTracker | undefined, ) { - this._changeSummary = this.createChangeSummary?.(); + this._changeSummary = this._changeTracker?.createChangeSummary(undefined); getLogger()?.handleAutorunCreated(this); this._run(); @@ -194,6 +188,9 @@ export class AutorunObserver implements IObserver, IReader } public dispose(): void { + if (this._disposed) { + return; + } this._disposed = true; for (const o of this._dependencies) { o.removeObserver(this); // Warning: external call! @@ -216,8 +213,11 @@ export class AutorunObserver implements IObserver, IReader getLogger()?.handleAutorunStarted(this); const changeSummary = this._changeSummary!; try { - this._changeSummary = this.createChangeSummary?.(); // Warning: external call! this._isRunning = true; + if (this._changeTracker) { + this._changeTracker.beforeUpdate?.(this, changeSummary); + this._changeSummary = this._changeTracker.createChangeSummary(changeSummary); // Warning: external call! + } this._runFn(this, changeSummary); // Warning: external call! } catch (e) { onBugIndicatingError(e); @@ -288,7 +288,7 @@ export class AutorunObserver implements IObserver, IReader getLogger()?.handleAutorunDependencyChanged(this, observable, change); try { // Warning: external call! - const shouldReact = this._handleChange ? this._handleChange({ + const shouldReact = this._changeTracker ? this._changeTracker.handleChange({ changedObservable: observable, change, didChange: (o): this is any => o === observable as any, diff --git a/code/src/vs/base/common/observableInternal/base.ts b/code/src/vs/base/common/observableInternal/base.ts index bab795322cf..94c454d75ab 100644 --- a/code/src/vs/base/common/observableInternal/base.ts +++ b/code/src/vs/base/common/observableInternal/base.ts @@ -570,21 +570,3 @@ export class DisposableObservableValue; - readonly change: unknown; - - /** - * Returns if the given observable caused the change. - */ - didChange(observable: IObservableWithChange): this is { change: TChange }; -} diff --git a/code/src/vs/base/common/observableInternal/changeTracker.ts b/code/src/vs/base/common/observableInternal/changeTracker.ts new file mode 100644 index 00000000000..8b160128978 --- /dev/null +++ b/code/src/vs/base/common/observableInternal/changeTracker.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../errors.js'; +import { IObservableWithChange, IReader } from './base.js'; + +export interface IChangeTracker { + createChangeSummary(previousChangeSummary: TChangeSummary | undefined): TChangeSummary; + handleChange(ctx: IChangeContext, change: TChangeSummary): boolean; + beforeUpdate?(reader: IReader, change: TChangeSummary): void; +} + +export interface IChangeContext { + readonly changedObservable: IObservableWithChange; + readonly change: unknown; + + /** + * Returns if the given observable caused the change. + */ + didChange(observable: IObservableWithChange): this is { change: TChange }; +} + +/** + * Subscribes to and records changes and the last value of the given observables. + * Don't use the key "changes", as it is reserved for the changes array! +*/ +export function recordChanges>>(obs: TObs): + IChangeTracker<{ [TKey in keyof TObs]: ReturnType } + & { changes: readonly ({ [TKey in keyof TObs]: { key: TKey; change: TObs[TKey]['TChange'] } }[keyof TObs])[] }> { + return { + createChangeSummary: (_previousChangeSummary) => { + return { + changes: [], + } as any; + }, + handleChange(ctx, changeSummary) { + for (const key in obs) { + if (ctx.didChange(obs[key])) { + (changeSummary.changes as any).push({ key, change: ctx.change }); + } + } + return true; + }, + beforeUpdate(reader, changeSummary) { + for (const key in obs) { + if (key === 'changes') { + throw new BugIndicatingError('property name "changes" is reserved for change tracking'); + } + changeSummary[key] = obs[key].read(reader); + } + } + }; +} diff --git a/code/src/vs/base/common/observableInternal/derived.ts b/code/src/vs/base/common/observableInternal/derived.ts index 59d8dc725a7..df876e5dee7 100644 --- a/code/src/vs/base/common/observableInternal/derived.ts +++ b/code/src/vs/base/common/observableInternal/derived.ts @@ -3,10 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseObservable, IChangeContext, IObservable, IObservableWithChange, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; +import { BaseObservable, IObservable, IObservableWithChange, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js'; import { BugIndicatingError, DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js'; import { getLogger } from './logging/logging.js'; +import { IChangeTracker } from './changeTracker.js'; + +export interface IDerivedReader extends IReader { + /** + * Call this to report a change delta or to force report a change, even if the new value is the same as the old value. + */ + reportChange(change: TChange): void; +} /** * Creates an observable that is derived from other observables. @@ -14,16 +22,15 @@ import { getLogger } from './logging/logging.js'; * * {@link computeFn} should start with a JS Doc using `@description` to name the derived. */ -export function derived(computeFn: (reader: IReader) => T): IObservable; -export function derived(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable; -export function derived(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable { +export function derived(computeFn: (reader: IDerivedReader) => T): IObservable; +export function derived(owner: DebugOwner, computeFn: (reader: IDerivedReader) => T): IObservable; +export function derived(computeFnOrOwner: ((reader: IDerivedReader) => T) | DebugOwner, computeFn?: ((reader: IDerivedReader) => T) | undefined): IObservable { if (computeFn !== undefined) { return new Derived( new DebugNameData(computeFnOrOwner, undefined, computeFn), computeFn, undefined, undefined, - undefined, strictEquals ); } @@ -32,7 +39,6 @@ export function derived(computeFnOrOwner: ((reader: IReader) => T) | DebugOwn computeFnOrOwner as any, undefined, undefined, - undefined, strictEquals ); } @@ -43,7 +49,6 @@ export function derivedWithSetter(owner: DebugOwner | undefined, computeFn: ( computeFn, undefined, undefined, - undefined, strictEquals, setter, ); @@ -60,7 +65,6 @@ export function derivedOpts( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn), computeFn, undefined, - undefined, options.onLastObserverRemoved, options.equalsFn ?? strictEquals ); @@ -83,8 +87,7 @@ _setDerivedOpts(derivedOpts); */ export function derivedHandleChanges( options: IDebugNameData & { - createEmptyChangeSummary: () => TChangeSummary; - handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean; + changeTracker: IChangeTracker; equalityComparer?: EqualityComparer; }, computeFn: (reader: IReader, changeSummary: TChangeSummary) => T @@ -92,8 +95,7 @@ export function derivedHandleChanges( return new Derived( new DebugNameData(options.owner, options.debugName, undefined), computeFn, - options.createEmptyChangeSummary, - options.handleChange, + options.changeTracker, undefined, options.equalityComparer ?? strictEquals ); @@ -125,7 +127,7 @@ export function derivedWithStore(computeFnOrOwner: ((reader: IReader, store: store.clear(); } return computeFn(r, store); - }, undefined, + }, undefined, () => store.dispose(), strictEquals, @@ -159,7 +161,7 @@ export function derivedDisposable(computeFnOr store.add(result); } return result; - }, undefined, + }, undefined, () => { if (store) { @@ -193,7 +195,7 @@ export const enum DerivedState { upToDate = 3, } -export class Derived extends BaseObservable implements IReader, IObserver { +export class Derived extends BaseObservable implements IDerivedReader, IObserver { private _state = DerivedState.initial; private _value: T | undefined = undefined; private _updateCount = 0; @@ -202,6 +204,7 @@ export class Derived extends BaseObservable im private _changeSummary: TChangeSummary | undefined = undefined; private _isUpdating = false; private _isComputing = false; + private _didReportChange = false; public override get debugName(): string { return this._debugNameData.getDebugName(this) ?? '(anonymous)'; @@ -209,14 +212,13 @@ export class Derived extends BaseObservable im constructor( public readonly _debugNameData: DebugNameData, - public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T, - private readonly createChangeSummary: (() => TChangeSummary) | undefined, - private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, + public readonly _computeFn: (reader: IDerivedReader, changeSummary: TChangeSummary) => T, + private readonly _changeTracker: IChangeTracker | undefined, private readonly _handleLastObserverRemoved: (() => void) | undefined = undefined, private readonly _equalityComparator: EqualityComparer, ) { super(); - this._changeSummary = this.createChangeSummary?.(); + this._changeSummary = this._changeTracker?.createChangeSummary(undefined); } protected override onLastObserverRemoved(): void { @@ -248,7 +250,12 @@ export class Derived extends BaseObservable im // Thus, we don't cache anything to prevent memory leaks. try { this._isReaderValid = true; - result = this._computeFn(this, this.createChangeSummary?.()!); + let changeSummary = undefined; + if (this._changeTracker) { + changeSummary = this._changeTracker.createChangeSummary(undefined); + this._changeTracker.beforeUpdate?.(this, changeSummary); + } + result = this._computeFn(this, changeSummary!); } finally { this._isReaderValid = false; } @@ -299,12 +306,16 @@ export class Derived extends BaseObservable im let didChange = false; this._isComputing = true; + this._didReportChange = false; try { const changeSummary = this._changeSummary!; - this._changeSummary = this.createChangeSummary?.(); try { this._isReaderValid = true; + if (this._changeTracker) { + this._changeTracker.beforeUpdate?.(this, changeSummary); + this._changeSummary = this._changeTracker?.createChangeSummary(changeSummary); + } /** might call {@link handleChange} indirectly, which could invalidate us */ this._value = this._computeFn(this, changeSummary); } finally { @@ -317,7 +328,7 @@ export class Derived extends BaseObservable im this._dependenciesToBeRemoved.clear(); } - didChange = hadValue && !(this._equalityComparator(oldValue!, this._value)); + didChange = this._didReportChange || (hadValue && !(this._equalityComparator(oldValue!, this._value))); getLogger()?.handleObservableUpdated(this, { oldValue, @@ -332,10 +343,12 @@ export class Derived extends BaseObservable im this._isComputing = false; - if (didChange) { + if (!this._didReportChange && didChange) { for (const r of this._observers) { r.handleChange(this, undefined); } + } else { + this._didReportChange = false; } } @@ -410,7 +423,7 @@ export class Derived extends BaseObservable im let shouldReact = false; try { - shouldReact = this._handleChange ? this._handleChange({ + shouldReact = this._changeTracker ? this._changeTracker.handleChange({ changedObservable: observable, change, didChange: (o): this is any => o === observable as any, @@ -447,6 +460,16 @@ export class Derived extends BaseObservable im return value; } + public reportChange(change: TChange): void { + if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } + + this._didReportChange = true; + // TODO add logging + for (const r of this._observers) { + r.handleChange(this, change); + } + } + public override addObserver(observer: IObserver): void { const shouldCallBeginUpdate = !this._observers.has(observer) && this._updateCount > 0; super.addObserver(observer); @@ -484,24 +507,30 @@ export class Derived extends BaseObservable im this._value = newValue as any; } + public setValue(newValue: T, tx: ITransaction, change: TChange): void { + this._value = newValue; + const observers = this._observers; + tx.updateObserver(this, this); + for (const d of observers) { + d.handleChange(this, change); + } + } } -export class DerivedWithSetter extends Derived implements ISettableObservable { +export class DerivedWithSetter extends Derived implements ISettableObservable { constructor( debugNameData: DebugNameData, - computeFn: (reader: IReader, changeSummary: TChangeSummary) => T, - createChangeSummary: (() => TChangeSummary) | undefined, - handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, + computeFn: (reader: IDerivedReader, changeSummary: TChangeSummary) => T, + changeTracker: IChangeTracker | undefined, handleLastObserverRemoved: (() => void) | undefined = undefined, equalityComparator: EqualityComparer, - public readonly set: (value: T, tx: ITransaction | undefined) => void, + public readonly set: (value: T, tx: ITransaction | undefined, change: TOutChanges) => void, ) { super( debugNameData, computeFn, - createChangeSummary, - handleChange, + changeTracker, handleLastObserverRemoved, equalityComparator, ); diff --git a/code/src/vs/base/common/observableInternal/index.ts b/code/src/vs/base/common/observableInternal/index.ts index c86501e702a..bc47cf25b7c 100644 --- a/code/src/vs/base/common/observableInternal/index.ts +++ b/code/src/vs/base/common/observableInternal/index.ts @@ -6,13 +6,14 @@ // 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 { 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 { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta } from './autorun.js'; +export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; +export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore, type IDerivedReader } 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'; +export { type IChangeContext, type IChangeTracker, recordChanges } from './changeTracker.js'; import { addLogger, setLogObservableFn } from './logging/logging.js'; import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js'; diff --git a/code/src/vs/base/common/observableInternal/logging/logging.ts b/code/src/vs/base/common/observableInternal/logging/logging.ts index dd6ba48416d..7e31bbe5393 100644 --- a/code/src/vs/base/common/observableInternal/logging/logging.ts +++ b/code/src/vs/base/common/observableInternal/logging/logging.ts @@ -54,8 +54,8 @@ export interface IObservableLogger { handleAutorunStarted(autorun: AutorunObserver): void; handleAutorunFinished(autorun: AutorunObserver): void; - handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void; - handleDerivedCleared(observable: Derived): void; + handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void; + handleDerivedCleared(observable: Derived): void; handleBeginTransaction(transaction: TransactionImpl): void; handleEndTransaction(transaction: TransactionImpl): void; diff --git a/code/src/vs/base/common/observableInternal/reducer.ts b/code/src/vs/base/common/observableInternal/reducer.ts new file mode 100644 index 00000000000..0408bb5ed2a --- /dev/null +++ b/code/src/vs/base/common/observableInternal/reducer.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EqualityComparer, strictEquals } from '../equals.js'; +import { BugIndicatingError } from '../errors.js'; +import { IObservable, IObservableWithChange, ISettableObservable, subtransaction } from './base.js'; +import { IChangeTracker } from './changeTracker.js'; +import { DebugNameData, DebugOwner } from './debugName.js'; +import { DerivedWithSetter, IDerivedReader } from './derived.js'; + +export interface IReducerOptions { + /** + * Is called to create the initial value of the observable when it becomes observed. + */ + initial: T | (() => T); + /** + * Is called to dispose the observable value when it is no longer observed. + */ + disposeFinal?(value: T): void; + changeTracker?: IChangeTracker; + equalityComparer?: EqualityComparer; + /** + * Applies the changes to the value. + * Use `reader.reportChange` to report change details or to report a change if the same value is returned. + */ + update(reader: IDerivedReader, previousValue: T, changes: TChangeSummary): T; +} + +/** + * Creates an observable value that is based on values and changes from other observables. + * Additionally, a reducer can report how that state changed. +*/ +export function observableReducer(owner: DebugOwner, options: IReducerOptions): SimplifyObservableWithChange { + return observableReducerSettable(owner, options) as any; +} + +/** + * Creates an observable value that is based on values and changes from other observables. + * Additionally, a reducer can report how that state changed. +*/ +export function observableReducerSettable(owner: DebugOwner, options: IReducerOptions): ISettableObservable { + let prevValue: T | undefined = undefined; + let hasValue = false; + + const d = new DerivedWithSetter( + new DebugNameData(owner, undefined, options.update), + (reader: IDerivedReader, changeSummary) => { + if (!hasValue) { + prevValue = options.initial instanceof Function ? options.initial() : options.initial; + hasValue = true; + } + const newValue = options.update(reader, prevValue!, changeSummary); + prevValue = newValue; + return newValue; + }, + options.changeTracker, + () => { + if (hasValue) { + options.disposeFinal?.(prevValue!); + hasValue = false; + } + }, + options.equalityComparer ?? strictEquals, + (value, tx, change) => { + if (!hasValue) { + throw new BugIndicatingError('Can only set when there is a listener! This is to prevent leaks.'); + } + subtransaction(tx, tx => { + prevValue = value; + d.setValue(value, tx, change); + }); + } + ); + + return d; +} + +/** + * Returns IObservable if TChange is void, otherwise IObservableWithChange +*/ +type SimplifyObservableWithChange = TChange extends void ? IObservable : IObservableWithChange; diff --git a/code/src/vs/base/common/observableInternal/utils.ts b/code/src/vs/base/common/observableInternal/utils.ts index 530474b334e..d914f96cc63 100644 --- a/code/src/vs/base/common/observableInternal/utils.ts +++ b/code/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. @@ -640,17 +641,19 @@ type RemoveUndefined = T extends undefined ? never : T; export function runOnChange(observable: IObservableWithChange, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[]) => void): IDisposable { let _previousValue: T | undefined; return autorunWithStoreHandleChanges({ - createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), - handleChange: (context, changeSummary) => { - if (context.didChange(observable)) { - const e = context.change; - if (e !== undefined) { - changeSummary.deltas.push(e as RemoveUndefined); + changeTracker: { + createChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), + handleChange: (context, changeSummary) => { + if (context.didChange(observable)) { + const e = context.change; + if (e !== undefined) { + changeSummary.deltas.push(e as RemoveUndefined); + } + changeSummary.didChange = true; } - changeSummary.didChange = true; - } - return true; - }, + return true; + }, + } }, (reader, changeSummary) => { const value = observable.read(reader); const previousValue = _previousValue; @@ -674,3 +677,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/code/src/vs/base/common/observableInternal/utilsCancellation.ts b/code/src/vs/base/common/observableInternal/utilsCancellation.ts index 17e4ba9e308..f76b58e1412 100644 --- a/code/src/vs/base/common/observableInternal/utilsCancellation.ts +++ b/code/src/vs/base/common/observableInternal/utilsCancellation.ts @@ -91,7 +91,6 @@ export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IRea cancellationTokenSource = new CancellationTokenSource(); return computeFn(r, cancellationTokenSource.token); }, undefined, - undefined, () => cancellationTokenSource?.dispose(), strictEquals ); diff --git a/code/src/vs/base/common/product.ts b/code/src/vs/base/common/product.ts index 26195d773ab..9e61a33545f 100644 --- a/code/src/vs/base/common/product.ts +++ b/code/src/vs/base/common/product.ts @@ -182,6 +182,10 @@ export interface IProductConfiguration { readonly extensionEnabledApiProposals?: { readonly [extensionId: string]: string[] }; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; + readonly extensionProperties: IStringDictionary<{ + readonly hasPrereleaseVersion?: boolean; + readonly excludeVersionRange?: string; + }>; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; @@ -331,6 +335,7 @@ export interface IDefaultChatAgent { readonly publicCodeMatchesUrl: string; readonly manageSettingsUrl: string; readonly managePlanUrl: string; + readonly manageOverageUrl: string; readonly upgradePlanUrl: string; readonly providerId: string; diff --git a/code/src/vs/base/common/sseParser.ts b/code/src/vs/base/common/sseParser.ts new file mode 100644 index 00000000000..0990e0247d9 --- /dev/null +++ b/code/src/vs/base/common/sseParser.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parser for Server-Sent Events (SSE) streams according to the HTML specification. + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + */ + +/** + * Represents an event dispatched from an SSE stream. + */ +export interface ISSEEvent { + /** + * The event type. If not specified, the type is "message". + */ + type: string; + + /** + * The event data. + */ + data: string; + + /** + * The last event ID, used for reconnection. + */ + id?: string; + + /** + * Reconnection time in milliseconds. + */ + retry?: number; +} + +/** + * Callback function type for event dispatch. + */ +export type SSEEventHandler = (event: ISSEEvent) => void; + +const enum Chr { + CR = 13, // '\r' + LF = 10, // '\n' + COLON = 58, // ':' + SPACE = 32, // ' ' +} + +/** + * Parser for Server-Sent Events (SSE) streams. + */ +export class SSEParser { + private dataBuffer = ''; + private eventTypeBuffer = ''; + private currentEventId?: string; + private lastEventIdBuffer?: string; + private reconnectionTime?: number; + private buffer: Uint8Array[] = []; + private endedOnCR = false; + private readonly onEventHandler: SSEEventHandler; + private readonly decoder: TextDecoder; + /** + * Creates a new SSE parser. + * @param onEvent The callback to invoke when an event is dispatched. + */ + constructor(onEvent: SSEEventHandler) { + this.onEventHandler = onEvent; + this.decoder = new TextDecoder('utf-8'); + } + + /** + * Gets the last event ID received by this parser. + */ + public getLastEventId(): string | undefined { + return this.lastEventIdBuffer; + } + /** + * Gets the reconnection time in milliseconds, if one was specified by the server. + */ + public getReconnectionTime(): number | undefined { + return this.reconnectionTime; + } + + /** + * Feeds a chunk of the SSE stream to the parser. + * @param chunk The chunk to parse as a Uint8Array of UTF-8 encoded data. + */ + public feed(chunk: Uint8Array): void { + if (chunk.length === 0) { + return; + } + + let offset = 0; + + // If the data stream was bifurcated between a CR and LF, avoid processing the CR as an extra newline + if (this.endedOnCR && chunk[0] === Chr.LF) { + offset++; + } + this.endedOnCR = false; + + // Process complete lines from the buffer + while (offset < chunk.length) { + const indexCR = chunk.indexOf(Chr.CR, offset); + const indexLF = chunk.indexOf(Chr.LF, offset); + const index = indexCR === -1 ? indexLF : (indexLF === -1 ? indexCR : Math.min(indexCR, indexLF)); + if (index === -1) { + break; + } + + let str = ''; + for (const buf of this.buffer) { + str += this.decoder.decode(buf, { stream: true }); + } + str += this.decoder.decode(chunk.subarray(offset, index)); + this.processLine(str); + + this.buffer.length = 0; + offset = index + (chunk[index] === Chr.CR && chunk[index + 1] === Chr.LF ? 2 : 1); + } + + + if (offset < chunk.length) { + this.buffer.push(chunk.subarray(offset)); + } else { + this.endedOnCR = chunk[chunk.length - 1] === Chr.CR; + } + } + /** + * Processes a single line from the SSE stream. + */ + private processLine(line: string): void { + if (!line.length) { + this.dispatchEvent(); + return; + } + + if (line.startsWith(':')) { + return; + } + + // Parse the field name and value + let field: string; + let value: string; + + const colonIndex = line.indexOf(':'); + if (colonIndex === -1) { + // Line with no colon - the entire line is the field name, value is empty + field = line; + value = ''; + } else { + // Line with a colon - split into field name and value + field = line.substring(0, colonIndex); + value = line.substring(colonIndex + 1); + + // If value starts with a space, remove it + if (value.startsWith(' ')) { + value = value.substring(1); + } + } + + this.processField(field, value); + } + /** + * Processes a field with the given name and value. + */ + private processField(field: string, value: string): void { + switch (field) { + case 'event': + this.eventTypeBuffer = value; + break; + + case 'data': + // Append the value to the data buffer, followed by a newline + this.dataBuffer += value; + this.dataBuffer += '\n'; + break; + + case 'id': + // If the field value doesn't contain NULL, set the last event ID buffer + if (!value.includes('\0')) { + this.currentEventId = this.lastEventIdBuffer = value; + } else { + this.currentEventId = undefined; + } + break; + + case 'retry': + // If the field value consists only of ASCII digits, set the reconnection time + if (/^\d+$/.test(value)) { + this.reconnectionTime = parseInt(value, 10); + } + break; + + // Ignore any other fields + } + } + /** + * Dispatches the event based on the current buffer states. + */ + private dispatchEvent(): void { + // If the data buffer is empty, reset the buffers and return + if (this.dataBuffer === '') { + this.dataBuffer = ''; + this.eventTypeBuffer = ''; + return; + } + + // If the data buffer's last character is a newline, remove it + if (this.dataBuffer.endsWith('\n')) { + this.dataBuffer = this.dataBuffer.substring(0, this.dataBuffer.length - 1); + } + + // Create and dispatch the event + const event: ISSEEvent = { + type: this.eventTypeBuffer || 'message', + data: this.dataBuffer, + }; + + // Add optional fields if they exist + if (this.currentEventId !== undefined) { + event.id = this.currentEventId; + } + + if (this.reconnectionTime !== undefined) { + event.retry = this.reconnectionTime; + } + + // Dispatch the event + this.onEventHandler(event); + + // Reset the data and event type buffers + this.reset(); + } + + /** + * Resets the parser state. + */ + public reset(): void { + this.dataBuffer = ''; + this.eventTypeBuffer = ''; + this.currentEventId = undefined; + // Note: lastEventIdBuffer is not reset as it's used for reconnection + } +} + + diff --git a/code/src/vs/base/common/stopwatch.ts b/code/src/vs/base/common/stopwatch.ts index e32c0dd9d91..ca3cb6388bc 100644 --- a/code/src/vs/base/common/stopwatch.ts +++ b/code/src/vs/base/common/stopwatch.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ // fake definition so that the valid layers check won't trip on this -declare const globalThis: { performance?: { now(): number } }; +declare const globalThis: { performance: { now(): number } }; -const hasPerformanceNow = (globalThis.performance && typeof globalThis.performance.now === 'function'); +const performanceNow = globalThis.performance.now.bind(globalThis.performance); export class StopWatch { @@ -20,7 +20,7 @@ export class StopWatch { } constructor(highResolution?: boolean) { - this._now = hasPerformanceNow && highResolution === false ? Date.now : globalThis.performance!.now.bind(globalThis.performance); + this._now = highResolution === false ? Date.now : performanceNow; this._startTime = this._now(); this._stopTime = -1; } diff --git a/code/src/vs/base/common/strings.ts b/code/src/vs/base/common/strings.ts index 1a8467170b5..1b12d1a4212 100644 --- a/code/src/vs/base/common/strings.ts +++ b/code/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/code/src/vs/base/common/ternarySearchTree.ts b/code/src/vs/base/common/ternarySearchTree.ts index 624b7c93327..fef1f8e1cf0 100644 --- a/code/src/vs/base/common/ternarySearchTree.ts +++ b/code/src/vs/base/common/ternarySearchTree.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { shuffle } from './arrays.js'; +import { assert } from './assert.js'; import { CharCode } from './charCode.js'; import { compare, compareIgnoreCase, compareSubstring, compareSubstringIgnoreCase } from './strings.js'; import { URI } from './uri.js'; @@ -264,11 +265,11 @@ abstract class Undef { class TernarySearchTreeNode { height: number = 1; segment!: string; - value: V | typeof Undef.Val | undefined; - key: K | undefined; - left: TernarySearchTreeNode | undefined; - mid: TernarySearchTreeNode | undefined; - right: TernarySearchTreeNode | undefined; + value: V | typeof Undef.Val | undefined = undefined; + key: K | undefined = undefined; + left: TernarySearchTreeNode | undefined = undefined; + mid: TernarySearchTreeNode | undefined = undefined; + right: TernarySearchTreeNode | undefined = undefined; isEmpty(): boolean { return !this.left && !this.mid && !this.right && this.value === undefined; @@ -563,13 +564,40 @@ export class TernarySearchTree { // full node // replace deleted-node with the min-node of the right branch. // If there is no true min-node leave things as they are - const min = this._min(node.right); + const stack2: typeof stack = [[Dir.Right, node]]; + const min = this._min(node.right, stack2); + if (min.key) { - const { key, value, segment } = min; - this._delete(min.key, false); - node.key = key; - node.value = value; - node.segment = segment; + + node.key = min.key; + node.value = min.value; + node.segment = min.segment; + + // remove NODE (inorder successor can only have right child) + const newChild = min.right; + if (stack2.length > 1) { + const [dir, parent] = stack2[stack2.length - 1]; + switch (dir) { + case Dir.Left: parent.left = newChild; break; + case Dir.Mid: assert(false); + case Dir.Right: assert(false); + } + } else { + node.right = newChild; + } + + // balance right branch and UPDATE parent pointer for stack + const newChild2 = this._balanceByStack(stack2)!; + if (stack.length > 0) { + const [dir, parent] = stack[stack.length - 1]; + switch (dir) { + case Dir.Left: parent.left = newChild2; break; + case Dir.Mid: parent.mid = newChild2; break; + case Dir.Right: parent.right = newChild2; break; + } + } else { + this._root = newChild2; + } } } else { @@ -589,6 +617,19 @@ export class TernarySearchTree { } // AVL balance + this._root = this._balanceByStack(stack) ?? this._root; + } + + private _min(node: TernarySearchTreeNode, stack: [Dir, TernarySearchTreeNode][]): TernarySearchTreeNode { + while (node.left) { + stack.push([Dir.Left, node]); + node = node.left; + } + return node; + } + + private _balanceByStack(stack: [Dir, TernarySearchTreeNode][]) { + for (let i = stack.length - 1; i >= 0; i--) { const node = stack[i][1]; @@ -631,16 +672,11 @@ export class TernarySearchTree { break; } } else { - this._root = stack[0][1]; + return stack[0][1]; } } - } - private _min(node: TernarySearchTreeNode): TernarySearchTreeNode { - while (node.left) { - node = node.left; - } - return node; + return undefined; } findSubstr(key: K): V | undefined { diff --git a/code/src/vs/base/node/nls.ts b/code/src/vs/base/node/nls.ts index c0784d05155..d47ef067a7d 100644 --- a/code/src/vs/base/node/nls.ts +++ b/code/src/vs/base/node/nls.ts @@ -123,9 +123,9 @@ export async function resolveNLSConfiguration({ userLocale, osLocale, userDataPa // ^moduleId ^nlsKeys ^moduleId ^nlsKey ^nlsValue = await Promise.all([ fs.promises.mkdir(commitLanguagePackCachePath, { recursive: true }), - JSON.parse(await fs.promises.readFile(path.join(nlsMetadataPath, 'nls.keys.json'), 'utf-8')), - JSON.parse(await fs.promises.readFile(path.join(nlsMetadataPath, 'nls.messages.json'), 'utf-8')), - JSON.parse(await fs.promises.readFile(mainLanguagePackPath, 'utf-8')) + fs.promises.readFile(path.join(nlsMetadataPath, 'nls.keys.json'), 'utf-8').then(content => JSON.parse(content)), + fs.promises.readFile(path.join(nlsMetadataPath, 'nls.messages.json'), 'utf-8').then(content => JSON.parse(content)), + fs.promises.readFile(mainLanguagePackPath, 'utf-8').then(content => JSON.parse(content)), ]); const nlsResult: string[] = []; diff --git a/code/src/vs/base/parts/sandbox/electron-sandbox/preload-slim.js b/code/src/vs/base/parts/sandbox/electron-sandbox/preload-slim.js deleted file mode 100644 index 7b6256756ac..00000000000 --- a/code/src/vs/base/parts/sandbox/electron-sandbox/preload-slim.js +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// @ts-check -(function () { - 'use strict'; - - const { ipcRenderer, webFrame, contextBridge } = require('electron'); - - /** - * @param {string} channel - * @returns {true | never} - */ - function validateIPC(channel) { - if (!channel || !channel.startsWith('vscode:')) { - throw new Error(`Unsupported event IPC channel '${channel}'`); - } - - return true; - } - - const globals = { - - /** - * A minimal set of methods exposed from Electron's `ipcRenderer` - * to support communication to main process. - * - * @typedef {Pick} IpcRenderer - * - * @type {IpcRenderer} - */ - ipcRenderer: { - - /** - * @param {string} channel - * @param {any[]} args - */ - send(channel, ...args) { - if (validateIPC(channel)) { - ipcRenderer.send(channel, ...args); - } - }, - - /** - * @param {string} channel - * @param {any[]} args - * @returns {Promise | never} - */ - invoke(channel, ...args) { - if (validateIPC(channel)) { - return ipcRenderer.invoke(channel, ...args); - } - } - }, - - /** - * Support for subset of methods of Electron's `webFrame` type. - * - * @type {import('./electronTypes').WebFrame} - */ - webFrame: { - - /** - * @param {number} level - */ - setZoomLevel(level) { - if (typeof level === 'number') { - webFrame.setZoomLevel(level); - } - } - } - }; - - try { - contextBridge.exposeInMainWorld('vscode', globals); - } catch (error) { - console.error(error); - } -}()); diff --git a/code/src/vs/base/test/common/async.test.ts b/code/src/vs/base/test/common/async.test.ts index c33c51aa0ff..5f34212928a 100644 --- a/code/src/vs/base/test/common/async.test.ts +++ b/code/src/vs/base/test/common/async.test.ts @@ -48,6 +48,22 @@ suite('Async', () => { return result; }); + test('cancel disposes result', function () { + + const store = new DisposableStore(); + + const promise = async.createCancelablePromise(async token => { + return store; + }); + promise.then(_ => assert.ok(false), err => { + + assert.ok(isCancellationError(err)); + assert.ok(store.isDisposed); + }); + + promise.cancel(); + }); + // Cancelling a sync cancelable promise will fire the cancelled token. // Also, every `then` callback runs in another execution frame. test('execution order (sync)', function () { diff --git a/code/src/vs/base/test/common/glob.test.ts b/code/src/vs/base/test/common/glob.test.ts index 3a57ae72492..74fbdf6f65d 100644 --- a/code/src/vs/base/test/common/glob.test.ts +++ b/code/src/vs/base/test/common/glob.test.ts @@ -1158,5 +1158,15 @@ suite('Glob', () => { assert.ok(!glob.patternsEquals(['a'], undefined)); }); + test('isEmptyPattern', () => { + assert.ok(glob.isEmptyPattern(glob.parse(''))); + assert.ok(glob.isEmptyPattern(glob.parse(undefined!))); + assert.ok(glob.isEmptyPattern(glob.parse(null!))); + + assert.ok(glob.isEmptyPattern(glob.parse({}))); + assert.ok(glob.isEmptyPattern(glob.parse({ '': true }))); + assert.ok(glob.isEmptyPattern(glob.parse({ '**/*.js': false }))); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/code/src/vs/base/test/common/observable.test.ts b/code/src/vs/base/test/common/observable.test.ts index 26b0517c740..6b640a275bb 100644 --- a/code/src/vs/base/test/common/observable.test.ts +++ b/code/src/vs/base/test/common/observable.test.ts @@ -7,9 +7,12 @@ import assert from 'assert'; 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'; -import { BaseObservable, IObservableWithChange } from '../../common/observableInternal/base.js'; +import { IDerivedReader, IObservableWithChange, autorun, autorunHandleChanges, autorunWithStoreHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, recordChanges, transaction, waitForState } from '../../common/observable.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal +import { BaseObservable } from '../../common/observableInternal/base.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal +import { observableReducer } from '../../common/observableInternal/reducer.js'; suite('observables', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); @@ -312,15 +315,17 @@ suite('observables', () => { const signal = observableSignal<{ msg: string }>('signal'); const disposable = autorunHandleChanges({ - // The change summary is used to collect the changes - createEmptyChangeSummary: () => ({ msgs: [] as string[] }), - handleChange(context, changeSummary) { - if (context.didChange(signal)) { - // We just push the changes into an array - changeSummary.msgs.push(context.change.msg); - } - return true; // We want to handle the change - }, + changeTracker: { + // The change summary is used to collect the changes + createChangeSummary: () => ({ msgs: [] as string[] }), + handleChange(context, changeSummary) { + if (context.didChange(signal)) { + // We just push the changes into an array + changeSummary.msgs.push(context.change.msg); + } + return true; // We want to handle the change + }, + } }, (reader, changeSummary) => { // When handling the change, make sure to read the signal! signal.read(reader); @@ -1527,6 +1532,87 @@ suite('observables', () => { disp.dispose(); }); }); + + suite('observableReducer', () => { + test('main', () => { + const store = new DisposableStore(); + const log = new Log(); + + const myObservable1 = observableValue('myObservable1', 5); + const myObservable2 = observableValue('myObservable2', 9); + + const sum = observableReducer(this, { + initial: () => { + log.log('createInitial'); + return myObservable1.get() + myObservable2.get(); + }, + disposeFinal: (values) => { + log.log(`disposeFinal ${values}`); + }, + changeTracker: recordChanges({ myObservable1, myObservable2 }), + update: (reader: IDerivedReader, previousValue, changes) => { + log.log(`update ${JSON.stringify(changes)}`); + let delta = 0; + for (const change of changes.changes) { + delta += change.change; + } + + reader.reportChange(delta); + const resultValue = previousValue + delta; + log.log(`update -> ${resultValue}`); + return resultValue; + } + }); + + assert.deepStrictEqual(log.getAndClearEntries(), ([])); + + store.add(autorunWithStoreHandleChanges({ + changeTracker: recordChanges({ sum }) + }, (_reader, changes) => { + log.log(`autorun ${JSON.stringify(changes)}`); + })); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + "createInitial", + 'update {"changes":[],"myObservable1":5,"myObservable2":9}', + "update -> 14", + 'autorun {"changes":[],"sum":14}', + ]); + + transaction(tx => { + myObservable1.set(myObservable1.get() + 1, tx, 1); + myObservable2.set(myObservable2.get() + 3, tx, 3); + }); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "update {\"changes\":[{\"key\":\"myObservable1\",\"change\":1},{\"key\":\"myObservable2\",\"change\":3}],\"myObservable1\":6,\"myObservable2\":12}", + "update -> 18", + "autorun {\"changes\":[{\"key\":\"sum\",\"change\":4}],\"sum\":18}" + ])); + + transaction(tx => { + myObservable1.set(myObservable1.get() + 1, tx, 1); + const s = sum.get(); + log.log(`sum.get() ${s}`); + myObservable2.set(myObservable2.get() + 3, tx, 3); + }); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "update {\"changes\":[{\"key\":\"myObservable1\",\"change\":1}],\"myObservable1\":7,\"myObservable2\":12}", + "update -> 19", + "sum.get() 19", + "update {\"changes\":[{\"key\":\"myObservable2\",\"change\":3}],\"myObservable1\":7,\"myObservable2\":15}", + "update -> 22", + "autorun {\"changes\":[{\"key\":\"sum\",\"change\":1}],\"sum\":22}" + ])); + + store.dispose(); + + assert.deepStrictEqual(log.getAndClearEntries(), ([ + "disposeFinal 22" + ])); + }); + }); }); export class LoggingObserver implements IObserver { diff --git a/code/src/vs/base/test/common/sseParser.test.ts b/code/src/vs/base/test/common/sseParser.test.ts new file mode 100644 index 00000000000..c87664b6fb1 --- /dev/null +++ b/code/src/vs/base/test/common/sseParser.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ISSEEvent, SSEParser } from '../../common/sseParser.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; + +// Helper function to convert string to Uint8Array for testing +function toUint8Array(str: string): Uint8Array { + return new TextEncoder().encode(str); +} + +suite('SSEParser', () => { + let receivedEvents: ISSEEvent[]; + let parser: SSEParser; + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + receivedEvents = []; + parser = new SSEParser((event) => receivedEvents.push(event)); + }); + test('handles basic events', () => { + parser.feed(toUint8Array('data: hello world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'message'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('handles events with multiple data fields', () => { + parser.feed(toUint8Array('data: first line\ndata: second line\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'first line\nsecond line'); + }); + test('handles events with explicit event type', () => { + parser.feed(toUint8Array('event: custom\ndata: hello world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('handles events with explicit event type (CRLF)', () => { + parser.feed(toUint8Array('event: custom\r\ndata: hello world\r\n\r\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('stream processing chunks', () => { + for (const lf of ['\n', '\r\n', '\r']) { + const message = toUint8Array(`event: custom${lf}data: hello world${lf}${lf}event: custom2${lf}data: hello world2${lf}${lf}`); + for (let chunkSize = 1; chunkSize < 5; chunkSize++) { + receivedEvents.length = 0; + + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.slice(i, i + chunkSize); + parser.feed(chunk); + } + + assert.deepStrictEqual(receivedEvents, [ + { type: 'custom', data: 'hello world' }, + { type: 'custom2', data: 'hello world2' } + ], `Failed for chunk size ${chunkSize} and line ending ${JSON.stringify(lf)}`); + } + } + }); + test('handles events with ID', () => { + parser.feed(toUint8Array('event: custom\ndata: hello\nid: 123\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].id, '123'); + assert.strictEqual(parser.getLastEventId(), '123'); + }); + + test('ignores comments', () => { + parser.feed(toUint8Array('event: custom\n:this is a comment\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles retry field', () => { + parser.feed(toUint8Array('retry: 5000\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].retry, 5000); + assert.strictEqual(parser.getReconnectionTime(), 5000); + }); + test('handles invalid retry field', () => { + parser.feed(toUint8Array('retry: invalid\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].retry, undefined); + assert.strictEqual(parser.getReconnectionTime(), undefined); + }); + + test('ignores fields with NULL character in ID', () => { + parser.feed(toUint8Array('id: 12\0 3\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].id, undefined); + assert.strictEqual(parser.getLastEventId(), undefined); + }); + + test('handles fields with no value', () => { + parser.feed(toUint8Array('data\nid\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, ''); + assert.strictEqual(receivedEvents[0].id, ''); + }); + test('handles fields with space after colon', () => { + parser.feed(toUint8Array('data: hello\nevent: custom\nid: 123\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].id, '123'); + }); + + test('handles different line endings (LF)', () => { + parser.feed(toUint8Array('data: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles different line endings (CR)', () => { + parser.feed(toUint8Array('data: hello\r\r')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles different line endings (CRLF)', () => { + parser.feed(toUint8Array('data: hello\r\n\r\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + test('handles empty data with blank line', () => { + parser.feed(toUint8Array('data:\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, ''); + }); + + test('ignores events with no data after blank line', () => { + parser.feed(toUint8Array('event: custom\n\n')); + + assert.strictEqual(receivedEvents.length, 0); + }); + + test('supports chunked data', () => { + parser.feed(toUint8Array('event: cus')); + parser.feed(toUint8Array('tom\nda')); + parser.feed(toUint8Array('ta: hello\n')); + parser.feed(toUint8Array('\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('supports spec example', () => { + // Example from the spec + parser.feed(toUint8Array(':This is a comment\ndata: first event\nid: 1\n\n')); + parser.feed(toUint8Array('data:second event\nid\n\n')); + parser.feed(toUint8Array('data: third event\n\n')); + + assert.strictEqual(receivedEvents.length, 3); + assert.strictEqual(receivedEvents[0].data, 'first event'); + assert.strictEqual(receivedEvents[0].id, '1'); + assert.strictEqual(receivedEvents[1].data, 'second event'); + assert.strictEqual(receivedEvents[1].id, ''); + assert.strictEqual(receivedEvents[2].data, ' third event'); + }); + + test('resets correctly', () => { + parser.feed(toUint8Array('data: hello\n')); + parser.reset(); + parser.feed(toUint8Array('data: world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'world'); + }); +}); diff --git a/code/src/vs/base/test/common/ternarySearchtree.test.ts b/code/src/vs/base/test/common/ternarySearchtree.test.ts index df36727e2d1..290ee4b4966 100644 --- a/code/src/vs/base/test/common/ternarySearchtree.test.ts +++ b/code/src/vs/base/test/common/ternarySearchtree.test.ts @@ -573,6 +573,43 @@ suite('Ternary Search Tree', () => { } }); + test('https://github.com/microsoft/vscode/issues/227147', function () { + + const raw = `fake-fs:CAOnRvUuxO,fake-fs:1qcbfq54rg,fake-fs:UtDstYUQ56,fake-fs:d5ktqDysll,fake-fs:w5NSAKA4Ch,fake-fs:QcIIIY6WHX,fake-fs:WCedQu9Ogd,fake-fs:cKUC5LunBr,fake-fs:XrIIYjI3HB,fake-fs:xgTkoneFzF,fake-fs:QYkCVx2nYC,fake-fs:ePrIDEKEpJ,fake-fs:nrOPYCW81a,fake-fs:MQbkFLcDsA,fake-fs:wXG8YiOrBI,fake-fs:4tHTWi240D,fake-fs:5uQWjgZGGJ,fake-fs:famP6pZXyx,fake-fs:aB9sUhwP1J,fake-fs:DlS0CssyhG,fake-fs:9vK2k3rL2V,fake-fs:iqWeu7zF6t,fake-fs:8vC6bQX2WH,fake-fs:nFILXMQTRg,fake-fs:miiV72aajE,fake-fs:9VRbqvaw0q,fake-fs:WnEHS1arfZ,fake-fs:Fco75PJ5pM,fake-fs:6CsEpoZ7VW,fake-fs:B2PrCtDpWu,fake-fs:y8Hi94Oekg,fake-fs:wyEjPNa5lo,fake-fs:zw1Ljv0erc,fake-fs:y4KWPUOMx0,fake-fs:1basrPTlTp,fake-fs:5iErr4YM34,fake-fs:Q2TQaujh8Q,fake-fs:QxcYzNNxZw,fake-fs:3QUDHjU55a,fake-fs:23ymf9ggMV,fake-fs:qQhuKFdy29,fake-fs:JuwmxA33oJ,fake-fs:NQeUyfMNUo,fake-fs:2Vo3eR1jxM,fake-fs:NzUXQidwel,fake-fs:aESYKGPxIx,fake-fs:mxLdeJartN,fake-fs:PhSd2xLwVe,fake-fs:9nmWjUUMRz,fake-fs:Wc6a4RsGhn,fake-fs:5a0AlFHALQ,fake-fs:Q93jnNZBxJ,fake-fs:4CuVkbfPSG,fake-fs:mdFlJ7WQva,fake-fs:fgVsaRm1KG,fake-fs:P7UXWiRJYj,fake-fs:q6nz5Q9BEW,fake-fs:1UZmGkvNTn,fake-fs:AKY8cnUQFl,fake-fs:RezYuPU7FD,fake-fs:5zaYc72Bit,fake-fs:yh8FTxFfQq,fake-fs:ayNPgEuc2q,fake-fs:EdOb27cRhF,fake-fs:h4c2uNyI4l,fake-fs:BhzOLNL4JO,fake-fs:HVPTdAMWpS,fake-fs:7K7IlacaZe,fake-fs:iUKJonC5eq,fake-fs:Y9E3NX3eJD,fake-fs:66h80uK32I,fake-fs:gFXpry1Y09,fake-fs:qOqvvXPcu4,fake-fs:UbbLn2NFSJ,fake-fs:TzJ07HsAGz,fake-fs:nQngmvgx4m,fake-fs:6bZQCR8epb,fake-fs:xb3SJKX1bi,fake-fs:GF3DPK4zDj,fake-fs:HmxgAqEegt,fake-fs:yT2OAMQYal,fake-fs:MiVX4VYXHk,fake-fs:QMbsUbjJTI,fake-fs:KzAbDNsmPc,fake-fs:m6CGOwOcdT,fake-fs:0cyHx9zsA3,fake-fs:SIwjWfFLSY,fake-fs:uZSDXCEqLY,fake-fs:HuoTL3nK7k,fake-fs:oyoejYE0CI,fake-fs:56WLhiCxbz,fake-fs:SqYOi0z5sM,fake-fs:LZq3ei28Ez,fake-fs:pTc4pCtwk8,fake-fs:AAJSFf0RHS,fake-fs:up6EHkEbO9,fake-fs:GB1Pesdnxd,fake-fs:Oyvq4Z96S4,fake-fs:rYXrhklgf6,fake-fs:g1HdUkQziH`; + const keys: URI[] = raw.split(',').map(value => URI.parse(value, true)); + + + const tst = TernarySearchTree.forUris(); + for (const item of keys) { + tst.set(item, true); + assert.ok(tst._isBalanced(), `SET${item}|${keys.map(String).join()}`); + } + + const lengthNow = Array.from(tst).length; + assert.strictEqual(lengthNow, keys.length); + + const keys2 = keys.slice(0); + + for (const [index, item] of keys.entries()) { + tst.delete(item); + assert.ok(tst._isBalanced(), `DEL${item}|${keys.map(String).join()}`); + + const idx = keys2.indexOf(item); + assert.ok(idx >= 0); + keys2.splice(idx, 1); + + const actualKeys = Array.from(tst).map(value => value[0]); + + assert.strictEqual( + actualKeys.length, + keys2.length, + `FAILED with ${index} -> ${item.toString()}\nWANTED:${keys2.map(String).sort().join()}\nACTUAL:${actualKeys.map(String).sort().join()}` + ); + } + + assert.strictEqual(Array.from(tst).length, 0); + }); + test('TernarySearchTree: Cannot read properties of undefined (reading \'length\'): #161618 (simple)', function () { const raw = 'config.debug.toolBarLocation,floating,config.editor.renderControlCharacters,true,config.editor.renderWhitespace,selection,config.files.autoSave,off,config.git.enabled,true,config.notebook.globalToolbar,true,config.terminal.integrated.tabs.enabled,true,config.terminal.integrated.tabs.showActions,singleTerminalOrNarrow,config.terminal.integrated.tabs.showActiveTerminal,singleTerminalOrNarrow,config.workbench.activityBar.visible,true,config.workbench.experimental.settingsProfiles.enabled,true,config.workbench.layoutControl.type,both,config.workbench.sideBar.location,left,config.workbench.statusBar.visible,true'; const array = raw.split(','); diff --git a/code/src/vs/code/electron-main/app.ts b/code/src/vs/code/electron-main/app.ts index 3fb0f3a0e78..588e1cb4521 100644 --- a/code/src/vs/code/electron-main/app.ts +++ b/code/src/vs/code/electron-main/app.ts @@ -168,7 +168,10 @@ export class CodeApplication extends Disposable { const isUrlFromWindow = (requestingUrl?: string | undefined) => requestingUrl?.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}`); const isUrlFromWebview = (requestingUrl: string | undefined) => requestingUrl?.startsWith(`${Schemas.vscodeWebview}://`); + const alwaysAllowedPermissions = new Set(['pointerLock']); + const allowedPermissionsInWebview = new Set([ + ...alwaysAllowedPermissions, 'clipboard-read', 'clipboard-sanitized-write', // TODO(deepak1556): Should be removed once migration is complete @@ -177,6 +180,7 @@ export class CodeApplication extends Disposable { ]); const allowedPermissionsInCore = new Set([ + ...alwaysAllowedPermissions, 'media', 'local-fonts', // TODO(deepak1556): Should be removed once migration is complete diff --git a/code/src/vs/code/electron-sandbox/workbench/workbench.ts b/code/src/vs/code/electron-sandbox/workbench/workbench.ts index 44a6051573c..ac30e72fd13 100644 --- a/code/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/code/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -165,8 +165,8 @@ } } - // part: side bar (only when opening workspace/folder) - if (configuration.workspace && layoutInfo.sideBarWidth > 0) { + // part: side bar + if (layoutInfo.sideBarWidth > 0) { const sideDiv = document.createElement('div'); sideDiv.style.position = 'absolute'; sideDiv.style.width = `${layoutInfo.sideBarWidth}px`; diff --git a/code/src/vs/editor/browser/config/editorConfiguration.ts b/code/src/vs/editor/browser/config/editorConfiguration.ts index 701c940c736..1bf70f155d7 100644 --- a/code/src/vs/editor/browser/config/editorConfiguration.ts +++ b/code/src/vs/editor/browser/config/editorConfiguration.ts @@ -130,7 +130,8 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat tabFocusMode: TabFocus.getTabFocusMode(), inputMode: InputMode.getInputMode(), accessibilitySupport: partialEnv.accessibilitySupport, - glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount + glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount, + editContextSupported: partialEnv.editContextSupported }; return EditorOptionsUtil.computeOptions(this._validatedOptions, env); } @@ -142,6 +143,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat outerHeight: this._containerObserver.getHeight(), emptySelectionClipboard: browser.isWebKit || browser.isFirefox, pixelRatio: PixelRatio.getInstance(getWindowById(this._targetWindowId, true).window).value, + editContextSupported: typeof (globalThis as any).EditContext === 'function', accessibilitySupport: ( this._accessibilityService.isScreenReaderOptimized() ? AccessibilitySupport.Enabled @@ -249,6 +251,7 @@ export interface IEnvConfiguration { emptySelectionClipboard: boolean; pixelRatio: number; accessibilitySupport: AccessibilitySupport; + editContextSupported: boolean; } class ValidatedEditorOptions implements IValidatedEditorOptions { diff --git a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css index 5ca36c59620..00170ceefc6 100644 --- a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css +++ b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css @@ -13,7 +13,7 @@ white-space: pre-wrap; } -.monaco-editor .native-edit-context-textarea { +.monaco-editor .ime-text-area { min-width: 0; min-height: 0; margin: 0; diff --git a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 9d17f5d9925..61a913ae7bc 100644 --- a/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/code/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -30,6 +30,8 @@ import { EditContext } from './editContextFactory.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { NativeEditContextRegistry } from './nativeEditContextRegistry.js'; import { IEditorAriaOptions } from '../../../editorBrowser.js'; +import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js'; +import { IME } from '../../../../../base/common/ime.js'; // Corresponds to classes in nativeEditContext.css enum CompositionClassName { @@ -38,11 +40,19 @@ enum CompositionClassName { PRIMARY = 'edit-context-composition-primary', } +interface ITextUpdateEvent { + text: string; + selectionStart: number; + selectionEnd: number; + updateRangeStart: number; + updateRangeEnd: number; +} + export class NativeEditContext extends AbstractEditContext { // Text area used to handle paste events - private readonly _textArea: FastDomNode; public readonly domNode: FastDomNode; + private readonly _imeTextArea: FastDomNode; private readonly _editContext: EditContext; private readonly _screenReaderSupport: ScreenReaderSupport; private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1); @@ -65,18 +75,19 @@ export class NativeEditContext extends AbstractEditContext { ownerID: string, context: ViewContext, overflowGuardContainer: FastDomNode, - viewController: ViewController, + private readonly _viewController: ViewController, private readonly _visibleRangeProvider: IVisibleRangeProvider, @IInstantiationService instantiationService: IInstantiationService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { super(context); this.domNode = new FastDomNode(document.createElement('div')); this.domNode.setClassName(`native-edit-context`); - this._textArea = new FastDomNode(document.createElement('textarea')); - this._textArea.setClassName('native-edit-context-textarea'); - this._textArea.setAttribute('tabindex', '-1'); + this._imeTextArea = new FastDomNode(document.createElement('textarea')); + this._imeTextArea.setClassName(`ime-text-area`); + this._imeTextArea.setAttribute('readonly', 'true'); + this._imeTextArea.setAttribute('tabindex', '-1'); this.domNode.setAttribute('autocorrect', 'off'); this.domNode.setAttribute('autocapitalize', 'off'); this.domNode.setAttribute('autocomplete', 'off'); @@ -85,13 +96,13 @@ export class NativeEditContext extends AbstractEditContext { this._updateDomAttributes(); overflowGuardContainer.appendChild(this.domNode); - overflowGuardContainer.appendChild(this._textArea); + overflowGuardContainer.appendChild(this._imeTextArea); this._parent = overflowGuardContainer.domNode; this._selectionChangeListener = this._register(new MutableDisposable()); this._focusTracker = this._register(new FocusTracker(this.domNode.domNode, (newFocusValue: boolean) => { if (newFocusValue) { - this._selectionChangeListener.value = this._setSelectionChangeListener(viewController); + this._selectionChangeListener.value = this._setSelectionChangeListener(this._viewController); this._screenReaderSupport.setIgnoreSelectionChangeTime('onFocus'); } else { this._selectionChangeListener.value = undefined; @@ -111,70 +122,94 @@ export class NativeEditContext extends AbstractEditContext { // result in a `selectionchange` event which we want to ignore this._screenReaderSupport.setIgnoreSelectionChangeTime('onCut'); this._ensureClipboardGetsEditorSelection(e); - viewController.cut(); + this._viewController.cut(); })); - this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => viewController.emitKeyUp(new StandardKeyboardEvent(e)))); - this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => { - - const standardKeyboardEvent = new StandardKeyboardEvent(e); - - // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further - if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { - standardKeyboardEvent.stopPropagation(); - } - viewController.emitKeyDown(standardKeyboardEvent); - })); + this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e))); + this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e))); + this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e))); + this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') { - this._onType(viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); + this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } })); + this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => { + e.preventDefault(); + if (!e.clipboardData) { + return; + } + let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); + if (!text) { + return; + } + metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); + let pasteOnNewLine = false; + let multicursorText: string[] | null = null; + let mode: string | null = null; + if (metadata) { + const options = this._context.configuration.options; + const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); + pasteOnNewLine = emptySelectionClipboard && !!metadata.isFromEmptySelection; + multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null; + mode = metadata.mode; + } + this._viewController.paste(text, pasteOnNewLine, multicursorText, mode); + })); // Edit context events this._register(editContextAddDisposableListener(this._editContext, 'textformatupdate', (e) => this._handleTextFormatUpdate(e))); this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e))); + let highSurrogateCharacter: string | undefined; this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { - this._emitTypeEvent(viewController, e); + const text = e.text; + if (text.length === 1) { + const charCode = text.charCodeAt(0); + if (isHighSurrogate(charCode)) { + highSurrogateCharacter = text; + return; + } + if (isLowSurrogate(charCode) && highSurrogateCharacter) { + const textUpdateEvent: ITextUpdateEvent = { + text: highSurrogateCharacter + text, + selectionEnd: e.selectionEnd, + selectionStart: e.selectionStart, + updateRangeStart: e.updateRangeStart - 1, + updateRangeEnd: e.updateRangeEnd - 1 + }; + highSurrogateCharacter = undefined; + this._emitTypeEvent(this._viewController, textUpdateEvent); + return; + } + } + this._emitTypeEvent(this._viewController, e); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => { // Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors - viewController.compositionStart(); + this._viewController.compositionStart(); // Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionStart(); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => { // Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors - viewController.compositionEnd(); + this._viewController.compositionEnd(); // Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionEnd(); })); - this._register(addDisposableListener(this._textArea.domNode, 'paste', (e) => { - // Pretend here we touched the text area, as the `paste` event will most likely - // result in a `selectionchange` event which we want to ignore - this._screenReaderSupport.setIgnoreSelectionChangeTime('onPaste'); - e.preventDefault(); - if (!e.clipboardData) { - return; + let reenableTracking: boolean = false; + this._register(IME.onDidChange(() => { + if (IME.enabled && reenableTracking) { + this.domNode.focus(); + this._focusTracker.resume(); + reenableTracking = false; } - let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - if (!text) { - return; - } - metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - let pasteOnNewLine = false; - let multicursorText: string[] | null = null; - let mode: string | null = null; - if (metadata) { - const options = this._context.configuration.options; - const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - pasteOnNewLine = emptySelectionClipboard && !!metadata.isFromEmptySelection; - multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null; - mode = metadata.mode; + if (!IME.enabled && this.isFocused()) { + this._focusTracker.pause(); + this._imeTextArea.focus(); + reenableTracking = true; } - viewController.paste(text, pasteOnNewLine, multicursorText, mode); })); this._register(NativeEditContextRegistry.register(ownerID, this)); } @@ -185,7 +220,7 @@ export class NativeEditContext extends AbstractEditContext { // Force blue the dom node so can write in pane with no native edit context after disposal this.domNode.domNode.blur(); this.domNode.domNode.remove(); - this._textArea.domNode.remove(); + this._imeTextArea.domNode.remove(); super.dispose(); } @@ -253,24 +288,8 @@ export class NativeEditContext extends AbstractEditContext { return true; } - public executePaste(): boolean { + public onWillPaste(): void { this._onWillPaste(); - try { - // pause focus tracking because we don't want to react to focus/blur - // events while pasting since we move the focus to the textarea - this._focusTracker.pause(); - - // Since we can not call execCommand('paste') on a dom node with edit context set - // we added a hidden text area that receives the paste execution - this._textArea.focus(); - const result = this._textArea.domNode.ownerDocument.execCommand('paste'); - this._textArea.domNode.textContent = ''; - this.domNode.focus(); - - return result; - } finally { - this._focusTracker.resume(); // resume focus tracking - } } private _onWillPaste(): void { @@ -309,6 +328,19 @@ export class NativeEditContext extends AbstractEditContext { // --- Private methods --- + private _onKeyUp(e: KeyboardEvent) { + this._viewController.emitKeyUp(new StandardKeyboardEvent(e)); + } + + private _onKeyDown(e: KeyboardEvent) { + const standardKeyboardEvent = new StandardKeyboardEvent(e); + // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further + if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { + standardKeyboardEvent.stopPropagation(); + } + this._viewController.emitKeyDown(standardKeyboardEvent); + } + private _updateDomAttributes(): void { const options = this._context.configuration.options; this.domNode.domNode.setAttribute('tabindex', String(options.get(EditorOption.tabIndex))); @@ -324,7 +356,7 @@ export class NativeEditContext extends AbstractEditContext { this._editContextPrimarySelection = editContextState.editContextPrimarySelection; } - private _emitTypeEvent(viewController: ViewController, e: TextUpdateEvent): void { + private _emitTypeEvent(viewController: ViewController, e: ITextUpdateEvent): void { if (!this._editContext) { return; } @@ -445,20 +477,19 @@ export class NativeEditContext extends AbstractEditContext { return; } const options = this._context.configuration.options; - const lineHeight = options.get(EditorOption.lineHeight); const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; const parentBounds = this._parent.getBoundingClientRect(); - const modelStartPosition = this._primarySelection.getStartPosition(); - const viewStartPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelStartPosition); - const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewStartPosition.lineNumber); + const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); + const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber); const top = parentBounds.top + verticalOffsetStart - this._scrollTop; - const height = (this._primarySelection.endLineNumber - this._primarySelection.startLineNumber + 1) * lineHeight; + const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); + const height = verticalOffsetEnd - verticalOffsetStart; let left = parentBounds.left + contentLeft - this._scrollLeft; let width: number; if (this._primarySelection.isEmpty()) { - const linesVisibleRanges = ctx.visibleRangeForPosition(viewStartPosition); + const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition()); if (linesVisibleRanges) { left += linesVisibleRanges.left; } @@ -478,7 +509,6 @@ export class NativeEditContext extends AbstractEditContext { } const options = this._context.configuration.options; const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const lineHeight = options.get(EditorOption.lineHeight); const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; const parentBounds = this._parent.getBoundingClientRect(); @@ -492,7 +522,8 @@ export class NativeEditContext extends AbstractEditContext { const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition); const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange); const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? []; - const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(characterViewRange.startLineNumber); + const lineNumber = characterViewRange.startLineNumber; + const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber); const top = parentBounds.top + characterVerticalOffset - this._scrollTop; let left = 0; @@ -504,6 +535,7 @@ export class NativeEditContext extends AbstractEditContext { break; } } + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber); characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight)); } this._editContext.updateCharacterBounds(e.rangeStart, characterBounds); @@ -543,7 +575,7 @@ export class NativeEditContext extends AbstractEditContext { let previousSelectionChangeEventTime = 0; return addDisposableListener(this.domNode.domNode.ownerDocument, 'selectionchange', () => { const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); - if (!this.isFocused() || !isScreenReaderOptimized) { + if (!this.isFocused() || !isScreenReaderOptimized || !IME.enabled) { return; } const screenReaderContentState = this._screenReaderSupport.screenReaderContentState; diff --git a/code/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/code/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index 93a03823220..dad5348efa9 100644 --- a/code/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/code/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -28,7 +28,6 @@ export class ScreenReaderSupport { private _contentWidth: number = 1; private _contentHeight: number = 1; private _divWidth: number = 1; - private _lineHeight: number = 1; private _fontInfo!: FontInfo; private _accessibilityPageSize: number = 1; private _ignoreSelectionChangeTime: number = 0; @@ -75,7 +74,6 @@ export class ScreenReaderSupport { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); this._divWidth = Math.round(wrappingColumn * this._fontInfo.typicalHalfwidthCharacterWidth); } @@ -133,10 +131,13 @@ export class ScreenReaderSupport { return; } - const offsetForStartPositionWithinEditor = this._context.viewLayout.getVerticalOffsetForLineNumber(this._screenReaderContentState.startPositionWithinEditor.lineNumber); - const offsetForPositionLineNumber = this._context.viewLayout.getVerticalOffsetForLineNumber(positionLineNumber); - const scrollTop = offsetForPositionLineNumber - offsetForStartPositionWithinEditor; - this._doRender(scrollTop, top, this._contentLeft, this._divWidth, this._lineHeight); + // The
where we render the screen reader content does not support variable line heights, + // all the lines must have the same height. We use the line height of the cursor position as the + // line height for all lines. + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(positionLineNumber); + const lineNumberWithinStateAboveCursor = positionLineNumber - this._screenReaderContentState.startPositionWithinEditor.lineNumber; + const scrollTop = lineNumberWithinStateAboveCursor * lineHeight; + this._doRender(scrollTop, top, this._contentLeft, this._divWidth, lineHeight); } private _renderAtTopLeft(): void { @@ -151,6 +152,7 @@ export class ScreenReaderSupport { this._domNode.setLeft(left); this._domNode.setWidth(width); this._domNode.setHeight(height); + this._domNode.setLineHeight(height); this._domNode.domNode.scrollTop = scrollTop; } @@ -177,9 +179,14 @@ export class ScreenReaderSupport { const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); if (isScreenReaderOptimized) { this._screenReaderContentState = this._getScreenReaderContentState(); - if (this._domNode.domNode.textContent !== this._screenReaderContentState.value) { + const endPosition = this._context.viewModel.model.getPositionAt(Infinity); + let value = this._screenReaderContentState.value; + if (endPosition.column === 1 && this._primarySelection.getEndPosition().equals(endPosition)) { + value += '\n'; + } + if (this._domNode.domNode.textContent !== value) { this.setIgnoreSelectionChangeTime('setValue'); - this._domNode.domNode.textContent = this._screenReaderContentState.value; + this._domNode.domNode.textContent = value; } this._setSelectionOfScreenReaderContent(this._screenReaderContentState.selectionStart, this._screenReaderContentState.selectionEnd); } else { diff --git a/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index a3f8b75544b..11a1f09ca04 100644 --- a/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -125,7 +125,6 @@ export class TextAreaEditContext extends AbstractEditContext { private _contentWidth: number; private _contentHeight: number; private _fontInfo: FontInfo; - private _lineHeight: number; private _emptySelectionClipboard: boolean; private _copyWithSyntaxHighlighting: boolean; @@ -169,7 +168,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); @@ -591,7 +589,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off'); @@ -745,6 +742,8 @@ export class TextAreaEditContext extends AbstractEditContext { } // Try to render the textarea with the color/font style to match the text under it + const viewPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(startPosition.lineNumber, 1)); + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber); const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1); const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1); @@ -753,7 +752,7 @@ export class TextAreaEditContext extends AbstractEditContext { (textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null) ); - this.textArea.domNode.scrollTop = lineCount * this._lineHeight; + this.textArea.domNode.scrollTop = lineCount * lineHeight; this.textArea.domNode.scrollLeft = scrollLeft; this._doRender({ @@ -761,7 +760,7 @@ export class TextAreaEditContext extends AbstractEditContext { top: top, left: left, width: width, - height: this._lineHeight, + height: lineHeight, useCover: false, color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground], italic: presentation.italic, @@ -798,19 +797,21 @@ export class TextAreaEditContext extends AbstractEditContext { if (platform.isMacintosh || this._accessibilitySupport === AccessibilitySupport.Enabled) { // For the popup emoji input, we will make the text area as high as the line height // We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers + const lineNumber = this._primaryCursorPosition.lineNumber; + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber); this._doRender({ lastRenderPosition: this._primaryCursorPosition, top, left: this._textAreaWrapping ? this._contentLeft : left, width: this._textAreaWidth, - height: this._lineHeight, + height: lineHeight, useCover: false }); // In case the textarea contains a word, we're going to try to align the textarea's cursor // with our cursor by scrolling the textarea as much as possible this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left; const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? newlinecount(this.textArea.domNode.value.substring(0, this.textArea.domNode.selectionStart)); - this.textArea.domNode.scrollTop = lineCount * this._lineHeight; + this.textArea.domNode.scrollTop = lineCount * lineHeight; return; } @@ -848,6 +849,7 @@ export class TextAreaEditContext extends AbstractEditContext { ta.setLeft(renderData.left); ta.setWidth(renderData.width); ta.setHeight(renderData.height); + ta.setLineHeight(renderData.height); ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : ''); ta.setFontStyle(renderData.italic ? 'italic' : ''); diff --git a/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index fc2dc0dd5ea..8e8d1e5c5a1 100644 --- a/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/code/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -619,19 +619,19 @@ export class TextAreaInput extends Disposable { export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrapper { - public readonly onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event; - public readonly onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event; - public readonly onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event; - public readonly onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event; - public readonly onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event; - public readonly onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event; - public readonly onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event; - public readonly onInput = >this._register(new DomEmitter(this._actual, 'input')).event; - public readonly onCut = this._register(new DomEmitter(this._actual, 'cut')).event; - public readonly onCopy = this._register(new DomEmitter(this._actual, 'copy')).event; - public readonly onPaste = this._register(new DomEmitter(this._actual, 'paste')).event; - public readonly onFocus = this._register(new DomEmitter(this._actual, 'focus')).event; - public readonly onBlur = this._register(new DomEmitter(this._actual, 'blur')).event; + public readonly onKeyDown: Event; + public readonly onKeyPress: Event; + public readonly onKeyUp: Event; + public readonly onCompositionStart: Event; + public readonly onCompositionUpdate: Event; + public readonly onCompositionEnd: Event; + public readonly onBeforeInput: Event; + public readonly onInput: Event; + public readonly onCut: Event; + public readonly onCopy: Event; + public readonly onPaste: Event; + public readonly onFocus: Event; + public readonly onBlur: Event; // = this._register(new DomEmitter(this._actual, 'blur')).event; public get ownerDocument(): Document { return this._actual.ownerDocument; @@ -647,12 +647,24 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap ) { super(); this._ignoreSelectionChangeTime = 0; + this.onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event; + this.onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event; + this.onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event; + this.onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event; + this.onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event; + this.onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event; + this.onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event; + this.onInput = >this._register(new DomEmitter(this._actual, 'input')).event; + this.onCut = this._register(new DomEmitter(this._actual, 'cut')).event; + this.onCopy = this._register(new DomEmitter(this._actual, 'copy')).event; + this.onPaste = this._register(new DomEmitter(this._actual, 'paste')).event; + this.onFocus = this._register(new DomEmitter(this._actual, 'focus')).event; + this.onBlur = this._register(new DomEmitter(this._actual, 'blur')).event; this._register(this.onKeyDown(() => inputLatency.onKeyDown())); this._register(this.onBeforeInput(() => inputLatency.onBeforeInput())); this._register(this.onInput(() => inputLatency.onInput())); this._register(this.onKeyUp(() => inputLatency.onKeyUp())); - this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire())); } diff --git a/code/src/vs/editor/browser/editorBrowser.ts b/code/src/vs/editor/browser/editorBrowser.ts index 082c14ea1d3..8f0ed8dabb8 100644 --- a/code/src/vs/editor/browser/editorBrowser.ts +++ b/code/src/vs/editor/browser/editorBrowser.ts @@ -19,7 +19,7 @@ import { IDiffComputationResult, ILineChange } from '../common/diff/legacyLinesD import * as editorCommon from '../common/editorCommon.js'; import { GlyphMarginLane, ICursorStateComputer, IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from '../common/model.js'; import { InjectedText } from '../common/modelLineProjectionData.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from '../common/textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelLineHeightChangedEvent } from '../common/textModelEvents.js'; import { IEditorWhitespace, IViewModel } from '../common/viewModel.js'; import { OverviewRulerZone } from '../common/viewModel/overviewZoneManager.js'; import { MenuId } from '../../platform/actions/common/actions.js'; @@ -891,6 +891,13 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getConfiguredWordAtPosition(position: Position): IWordAtPosition | null; + /** + * An event emitted when line heights from decorations change + * @internal + * @event + */ + onDidChangeLineHeight: Event; + /** * Get value of the current model attached to this editor. * @see {@link ITextModel.getValue} @@ -1019,7 +1026,7 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * @internal */ - setDecorationsByType(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void; + setDecorationsByType(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): readonly string[]; /** * @internal @@ -1068,6 +1075,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the line height for the line number. + */ + getLineHeightForLineNumber(lineNumber: number): number; + /** * Set the model ranges that will be hidden in the view. * Hidden areas are stored per source. diff --git a/code/src/vs/editor/browser/gpu/objectCollectionBuffer.ts b/code/src/vs/editor/browser/gpu/objectCollectionBuffer.ts index a61fe8c09de..9a918e1abcb 100644 --- a/code/src/vs/editor/browser/gpu/objectCollectionBuffer.ts +++ b/code/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/code/src/vs/editor/browser/services/abstractCodeEditorService.ts b/code/src/vs/editor/browser/services/abstractCodeEditorService.ts index 295d5bf1ca1..39a0bce8add 100644 --- a/code/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/code/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -460,6 +460,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { public afterContentClassName: string | undefined; public glyphMarginClassName: string | undefined; public isWholeLine: boolean; + public lineHeight?: number; public overviewRuler: IModelDecorationOverviewRulerOptions | undefined; public stickiness: TrackedRangeStickiness | undefined; public beforeInjectedText: InjectedTextOptions | undefined; @@ -520,6 +521,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { const options = providerArgs.options; this.isWholeLine = Boolean(options.isWholeLine); + this.lineHeight = options.lineHeight; this.stickiness = options.rangeBehavior; const lightOverviewRulerColor = options.light && options.light.overviewRulerColor || options.overviewRulerColor; @@ -549,6 +551,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { className: this.className, glyphMarginClassName: this.glyphMarginClassName, isWholeLine: this.isWholeLine, + lineHeight: this.lineHeight, overviewRuler: this.overviewRuler, stickiness: this.stickiness, before: this.beforeInjectedText, diff --git a/code/src/vs/editor/browser/services/hoverService/hover.css b/code/src/vs/editor/browser/services/hoverService/hover.css index 8d483a54163..d7c20fbcde9 100644 --- a/code/src/vs/editor/browser/services/hoverService/hover.css +++ b/code/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/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts index c33296a34e6..9635e03c818 100644 --- a/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -61,6 +61,7 @@ export class HoverWidget extends Widget implements IHoverWidget { private _isLocked: boolean = false; private _enableFocusTraps: boolean = false; private _addedFocusTrap: boolean = false; + private _maxHeightRatioRelativeToWindow: number = 0.5; private get _targetWindow(): Window { return dom.getWindow(this._target.targetElements[0]); @@ -127,6 +128,11 @@ export class HoverWidget extends Widget implements IHoverWidget { this._enableFocusTraps = true; } + const maxHeightRatio = options.appearance?.maxHeightRatio; + if (maxHeightRatio !== undefined && maxHeightRatio > 0 && maxHeightRatio <= 1) { + this._maxHeightRatioRelativeToWindow = maxHeightRatio; + } + // Default to position above when the position is unspecified or a mouse event this._hoverPosition = options.position?.hoverPosition === undefined ? HoverPosition.ABOVE @@ -551,7 +557,7 @@ export class HoverWidget extends Widget implements IHoverWidget { } private adjustHoverMaxHeight(target: TargetRect): void { - let maxHeight = this._targetWindow.innerHeight / 2; + let maxHeight = this._targetWindow.innerHeight * this._maxHeightRatioRelativeToWindow; // When force position is enabled, restrict max height if (this._forcePosition) { diff --git a/code/src/vs/editor/browser/view/renderingContext.ts b/code/src/vs/editor/browser/view/renderingContext.ts index e5ec4b30eca..6fe17c1a9f7 100644 --- a/code/src/vs/editor/browser/view/renderingContext.ts +++ b/code/src/vs/editor/browser/view/renderingContext.ts @@ -61,6 +61,10 @@ export abstract class RestrictedRenderingContext { return this._viewLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._viewLayout.getLineHeightForLineNumber(lineNumber); + } + public getDecorationsInViewport(): ViewModelDecoration[] { return this.viewportData.getDecorationsInViewport(); } diff --git a/code/src/vs/editor/browser/view/viewLayer.ts b/code/src/vs/editor/browser/view/viewLayer.ts index 4058205be2f..1b64760a7f4 100644 --- a/code/src/vs/editor/browser/view/viewLayer.ts +++ b/code/src/vs/editor/browser/view/viewLayer.ts @@ -10,6 +10,7 @@ import { EditorOption } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import * as viewEvents from '../../common/viewEvents.js'; import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import { ViewContext } from '../../common/viewModel/viewContext.js'; /** * Represents a visible line @@ -255,7 +256,8 @@ export class VisibleLinesCollection { private readonly _linesCollection: RenderedLinesCollection = new RenderedLinesCollection(this._lineFactory); constructor( - private readonly _lineFactory: ILineFactory + private readonly _viewContext: ViewContext, + private readonly _lineFactory: ILineFactory, ) { } @@ -354,7 +356,7 @@ export class VisibleLinesCollection { const inp = this._linesCollection._get(); - const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData); + const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData, this._viewContext); const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, @@ -383,6 +385,7 @@ class ViewLayerRenderer { private readonly _domNode: HTMLElement, private readonly _lineFactory: ILineFactory, private readonly _viewportData: ViewportData, + private readonly _viewContext: ViewContext ) { } @@ -467,7 +470,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._viewportData.lineHeight); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._lineHeightForLineNumber(lineNumber)); } } @@ -571,7 +574,8 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this._viewportData.lineHeight, this._viewportData, sb); + const renderedLineNumber = i + rendLineNumberStart; + const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -601,7 +605,8 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this._viewportData.lineHeight, this._viewportData, sb); + const renderedLineNumber = i + rendLineNumberStart; + const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -616,4 +621,8 @@ class ViewLayerRenderer { } } } + + private _lineHeightForLineNumber(lineNumber: number): number { + return this._viewContext.viewLayout.getLineHeightForLineNumber(lineNumber); + } } diff --git a/code/src/vs/editor/browser/view/viewOverlays.ts b/code/src/vs/editor/browser/view/viewOverlays.ts index 6b8e10f341c..1f351bb3571 100644 --- a/code/src/vs/editor/browser/view/viewOverlays.ts +++ b/code/src/vs/editor/browser/view/viewOverlays.ts @@ -24,7 +24,7 @@ export class ViewOverlays extends ViewPart { constructor(context: ViewContext) { super(context); - this._visibleLines = new VisibleLinesCollection({ + this._visibleLines = new VisibleLinesCollection(this._context, { createLine: () => new ViewOverlayLine(this._dynamicOverlays) }); this.domNode = this._visibleLines.domNode; @@ -178,6 +178,8 @@ export class ViewOverlayLine implements IVisibleLine { sb.appendString(String(deltaTop)); sb.appendString('px;height:'); sb.appendString(String(lineHeight)); + sb.appendString('px;line-height:'); + sb.appendString(String(lineHeight)); sb.appendString('px;">'); sb.appendString(result); sb.appendString('
'); @@ -189,6 +191,7 @@ export class ViewOverlayLine implements IVisibleLine { if (this._domNode) { this._domNode.setTop(deltaTop); this._domNode.setHeight(lineHeight); + this._domNode.setLineHeight(lineHeight); } } } diff --git a/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 6ae696f90dd..f3e89362b99 100644 --- a/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/code/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -198,7 +198,6 @@ class Widget { private readonly _fixedOverflowWidgets: boolean; private _contentWidth: number; private _contentLeft: number; - private _lineHeight: number; private _primaryAnchor: PositionPair = new PositionPair(null, null); private _secondaryAnchor: PositionPair = new PositionPair(null, null); @@ -227,7 +226,6 @@ class Widget { this._fixedOverflowWidgets = options.get(EditorOption.fixedOverflowWidgets); this._contentWidth = layoutInfo.contentWidth; this._contentLeft = layoutInfo.contentLeft; - this._lineHeight = options.get(EditorOption.lineHeight); this._affinity = null; this._preference = []; @@ -246,7 +244,6 @@ class Widget { public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); if (e.hasChanged(EditorOption.layoutInfo)) { const layoutInfo = options.get(EditorOption.layoutInfo); this._contentLeft = layoutInfo.contentLeft; @@ -403,12 +400,12 @@ class Widget { * The content widget should touch if possible the secondary anchor. */ private _getAnchorsCoordinates(ctx: RenderingContext): { primary: AnchorCoordinate | null; secondary: AnchorCoordinate | null } { - const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity, this._lineHeight); + const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity); const secondaryViewPosition = (this._secondaryAnchor.viewPosition?.lineNumber === this._primaryAnchor.viewPosition?.lineNumber ? this._secondaryAnchor.viewPosition : null); - const secondary = getCoordinates(secondaryViewPosition, this._affinity, this._lineHeight); + const secondary = getCoordinates(secondaryViewPosition, this._affinity); return { primary, secondary }; - function getCoordinates(position: Position | null, affinity: PositionAffinity | null, lineHeight: number): AnchorCoordinate | null { + function getCoordinates(position: Position | null, affinity: PositionAffinity | null): AnchorCoordinate | null { if (!position) { return null; } @@ -421,6 +418,7 @@ class Widget { // Left-align widgets that should appear :before content const left = (position.column === 1 && affinity === PositionAffinity.LeftOfInjectedText ? 0 : horizontalPosition.left); const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop; + const lineHeight = ctx.getLineHeightForLineNumber(position.lineNumber); return new AnchorCoordinate(top, left, lineHeight); } } diff --git a/code/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/code/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index c1234141862..dd565eac9e4 100644 --- a/code/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/code/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -415,7 +415,8 @@ export class GlyphMarginWidgets extends ViewPart { // Render decorations, reusing previous dom nodes as possible for (let i = 0; i < this._decorationGlyphsToRender.length; i++) { const dec = this._decorationGlyphsToRender[i]; - const top = ctx.viewportData.relativeVerticalOffset[dec.lineNumber - ctx.viewportData.startLineNumber]; + const decLineNumber = dec.lineNumber; + const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber]; const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight; let domNode: FastDomNode; @@ -426,13 +427,14 @@ export class GlyphMarginWidgets extends ViewPart { this._managedDomNodes.push(domNode); this.domNode.appendChild(domNode); } + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber); domNode.setClassName(`cgmr codicon ` + dec.combinedClassName); domNode.setPosition(`absolute`); domNode.setTop(top); domNode.setLeft(left); domNode.setWidth(width); - domNode.setHeight(this._lineHeight); + domNode.setHeight(lineHeight); } // remove extra dom nodes diff --git a/code/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/code/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index b4380373c52..1e4164e9f81 100644 --- a/code/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/code/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -47,7 +47,6 @@ export class ViewCursor { private _cursorStyle: TextEditorCursorStyle; private _lineCursorWidth: number; - private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _isVisible: boolean; @@ -64,7 +63,6 @@ export class ViewCursor { const fontInfo = options.get(EditorOption.fontInfo); this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); @@ -73,7 +71,7 @@ export class ViewCursor { // Create the dom node this._domNode = createFastDomNode(document.createElement('div')); this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); - this._domNode.setHeight(this._lineHeight); + this._domNode.setHeight(this._context.viewLayout.getLineHeightForLineNumber(1)); this._domNode.setTop(0); this._domNode.setLeft(0); applyFontInfo(this._domNode, fontInfo); @@ -131,7 +129,6 @@ export class ViewCursor { const fontInfo = options.get(EditorOption.fontInfo); this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); applyFontInfo(this._domNode, fontInfo); @@ -193,7 +190,8 @@ export class ViewCursor { } const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.bigNumbersDelta; - return new ViewCursorRenderData(top, left, paddingLeft, width, this._lineHeight, textContent, textContentClassName); + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(position.lineNumber); + return new ViewCursorRenderData(top, left, paddingLeft, width, lineHeight, textContent, textContentClassName); } const visibleRangeForCharacter = ctx.linesVisibleRangesForRange(new Range(position.lineNumber, position.column, position.lineNumber, position.column + nextGrapheme.length), false); @@ -223,11 +221,12 @@ export class ViewCursor { } let top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.bigNumbersDelta; - let height = this._lineHeight; + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(position.lineNumber); + let height = lineHeight; // Underline might interfere with clicking if (this._cursorStyle === TextEditorCursorStyle.Underline || this._cursorStyle === TextEditorCursorStyle.UnderlineThin) { - top += this._lineHeight - 2; + top += lineHeight - 2; height = 2; } diff --git a/code/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/code/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 16229cd928a..476bdeeb68c 100644 --- a/code/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/code/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -175,6 +175,8 @@ export class ViewLine implements IVisibleLine { sb.appendString(String(deltaTop)); sb.appendString('px;height:'); sb.appendString(String(lineHeight)); + sb.appendString('px;line-height:'); + sb.appendString(String(lineHeight)); sb.appendString('px;" class="'); sb.appendString(ViewLine.CLASS_NAME); sb.appendString('">'); @@ -211,6 +213,7 @@ export class ViewLine implements IVisibleLine { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); this._renderedViewLine.domNode.setHeight(lineHeight); + this._renderedViewLine.domNode.setLineHeight(lineHeight); } } diff --git a/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index 78324773a68..140b62be89e 100644 --- a/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/code/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -145,7 +145,7 @@ export class ViewLines extends ViewPart implements IViewLines { this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = new VisibleLinesCollection({ + this._visibleLines = new VisibleLinesCollection(this._context, { createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions), }); this.domNode = this._visibleLines.domNode; @@ -444,7 +444,7 @@ export class ViewLines extends ViewPart implements IViewLines { } const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1; - const continuesInNextLine = lineNumber !== range.endLineNumber; + const continuesInNextLine = lineNumber !== originalEndLineNumber; const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn; const visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(lineNumber, startColumn, endColumn, domReadingContext); diff --git a/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index a6cabd55951..dd0666b3e3b 100644 --- a/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/code/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -535,7 +535,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { continue; } const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1; - const continuesInNextLine = lineNumber !== range.endLineNumber; + const continuesInNextLine = lineNumber !== originalEndLineNumber; const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn; const visibleRangesForLine = this._visibleRangesForLineRange(lineNumber, startColumn, endColumn); diff --git a/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 56cc4692dc0..d26aba26a26 100644 --- a/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/code/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -146,7 +146,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { const fauxIndentLength = lineData.minColumn - 1; const onlyBoundary = (this._options.renderWhitespace === 'boundary'); const onlyTrailing = (this._options.renderWhitespace === 'trailing'); - const lineHeight = this._options.lineHeight; + const lineHeight = ctx.getLineHeightForLineNumber(lineNumber); const middotWidth = this._options.middotWidth; const wsmiddotWidth = this._options.wsmiddotWidth; const spaceWidth = this._options.spaceWidth; diff --git a/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index fc0a05c5ede..f2a45afad39 100644 --- a/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/code/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -44,7 +44,7 @@ import { EndOfLinePreference, IAttachedView, ICursorStateComputer, IIdentifiedSi import { ClassName } from '../../../common/model/intervalTree.js'; import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from '../../../common/textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelLineHeightChangedEvent } from '../../../common/textModelEvents.js'; import { VerticalRevealType } from '../../../common/viewEvents.js'; import { IEditorWhitespace, IViewModel } from '../../../common/viewModel.js'; import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; @@ -91,6 +91,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidChangeModelDecorations: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelDecorations: Event = this._onDidChangeModelDecorations.event; + private readonly _onDidChangeLineHeight: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); + public readonly onDidChangeLineHeight: Event = this._onDidChangeLineHeight.event; + private readonly _onDidChangeModelTokens: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelTokens: Event = this._onDidChangeModelTokens.event; @@ -594,6 +597,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + if (!this._modelData) { + return -1; + } + const viewPosition = this._modelData.viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)); + return this._modelData.viewModel.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); + } + public setHiddenAreas(ranges: IRange[], source?: unknown, forceUpdate?: boolean): void { this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source, forceUpdate); } @@ -1314,7 +1325,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }); } - public setDecorationsByType(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void { + public setDecorationsByType(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): readonly string[] { const newDecorationsSubTypes: { [key: string]: boolean } = {}; const oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {}; @@ -1354,6 +1365,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE // update all decorations const oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey] || []; this.changeDecorations(accessor => this._decorationTypeKeysToIds[decorationTypeKey] = accessor.deltaDecorations(oldDecorationsIds, newModelDecorations)); + return this._decorationTypeKeysToIds[decorationTypeKey] || []; } public setDecorationsByTypeFast(decorationTypeKey: string, ranges: IRange[]): void { @@ -1597,11 +1609,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const top = CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, position.lineNumber, position.column) - this.getScrollTop(); const left = this._modelData.view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.glyphMarginWidth + layoutInfo.lineNumbersWidth + layoutInfo.decorationsWidth - this.getScrollLeft(); - + const height = this.getLineHeightForLineNumber(position.lineNumber); return { top: top, left: left, - height: options.get(EditorOption.lineHeight) + height }; } @@ -1775,6 +1787,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE case OutgoingViewModelEventKind.ModelTokensChanged: this._onDidChangeModelTokens.fire(e.event); break; + case OutgoingViewModelEventKind.ModelLineHeightChanged: + this._onDidChangeLineHeight.fire(e.event); + break; } })); diff --git a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts index 638c9fe8bd5..3dfb032516c 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts @@ -61,12 +61,14 @@ export class DiffEditorEditors extends Disposable { this._argCodeEditorWidgetOptions = null as any; this._register(autorunHandleChanges({ - createEmptyChangeSummary: (): IDiffEditorConstructionOptions => ({}), - handleChange: (ctx, changeSummary) => { - if (ctx.didChange(_options.editorOptions)) { - Object.assign(changeSummary, ctx.change.changedOptions); + changeTracker: { + createChangeSummary: (): IDiffEditorConstructionOptions => ({}), + handleChange: (ctx, changeSummary) => { + if (ctx.didChange(_options.editorOptions)) { + Object.assign(changeSummary, ctx.change.changedOptions); + } + return true; } - return true; } }, (reader, changeSummary) => { /** @description update editor options */ diff --git a/code/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/code/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index a06947da06d..cbf57933eb5 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -95,11 +95,13 @@ export class MovedBlocksLinesFeature extends Disposable { let lastChangedEditor: 'original' | 'modified' = 'modified'; this._register(autorunHandleChanges({ - createEmptyChangeSummary: () => undefined, - handleChange: (ctx, summary) => { - if (ctx.didChange(originalHasFocus)) { lastChangedEditor = 'original'; } - if (ctx.didChange(modifiedHasFocus)) { lastChangedEditor = 'modified'; } - return true; + changeTracker: { + createChangeSummary: () => undefined, + handleChange: (ctx, summary) => { + if (ctx.didChange(originalHasFocus)) { lastChangedEditor = 'original'; } + if (ctx.didChange(modifiedHasFocus)) { lastChangedEditor = 'modified'; } + return true; + } } }, reader => { /** @description MovedBlocksLines.setActiveMovedTextFromCursor */ diff --git a/code/src/vs/editor/browser/widget/diffEditor/utils.ts b/code/src/vs/editor/browser/widget/diffEditor/utils.ts index 47faca2f8cf..8fbcaddc48a 100644 --- a/code/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/code/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -137,12 +137,14 @@ export function animatedObservable(targetWindow: Window, base: IObservableWithCh let animationFrame: number | undefined = undefined; store.add(autorunHandleChanges({ - createEmptyChangeSummary: () => ({ animate: false }), - handleChange: (ctx, s) => { - if (ctx.didChange(base)) { - s.animate = s.animate || ctx.change; + changeTracker: { + createChangeSummary: () => ({ animate: false }), + handleChange: (ctx, s) => { + if (ctx.didChange(base)) { + s.animate = s.animate || ctx.change; + } + return true; } - return true; } }, (reader, s) => { /** @description update value */ @@ -328,14 +330,16 @@ export function applyViewZones(editor: ICodeEditor, viewZones: IObservable { /** @description layoutZone on change */ for (const vz of curViewZones) { diff --git a/code/src/vs/editor/common/codecs/baseToken.ts b/code/src/vs/editor/common/codecs/baseToken.ts index 6430ffb61a5..99c4df36758 100644 --- a/code/src/vs/editor/common/codecs/baseToken.ts +++ b/code/src/vs/editor/common/codecs/baseToken.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { pick } from '../../../base/common/arrays.js'; +import { assert } from '../../../base/common/assert.js'; import { IRange, Range } from '../../../editor/common/core/range.js'; /** @@ -59,4 +61,107 @@ export abstract class BaseToken { return this; } + + /** + * Render a list of tokens into a string. + */ + public static render(tokens: readonly BaseToken[]): string { + return tokens.map(pick('text')).join(''); + } + + /** + * Returns the full range of a list of tokens in which the first token is + * used as the start of a tokens sequence and the last token reflects the end. + * + * @throws if: + * - provided {@link tokens} list is empty + * - the first token start number is greater than the start line of the last token + * - if the first and last token are on the same line, the first token start column must + * be smaller than the start column of the last token + */ + public static fullRange(tokens: readonly BaseToken[]): Range { + assert( + tokens.length > 0, + 'Cannot get full range for an empty list of tokens.', + ); + + const firstToken = tokens[0]; + const lastToken = tokens[tokens.length - 1]; + + // sanity checks for the full range we would construct + assert( + firstToken.range.startLineNumber <= lastToken.range.startLineNumber, + 'First token must start on previous or the same line as the last token.', + ); + if ((firstToken !== lastToken) && (firstToken.range.startLineNumber === lastToken.range.startLineNumber)) { + assert( + firstToken.range.endColumn <= lastToken.range.startColumn, + [ + 'First token must end at least on previous or the same column as the last token.', + `First token: ${firstToken}; Last token: ${lastToken}.`, + ].join('\n'), + ); + } + + return new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ); + } + + /** + * Shorten version of the {@link text} property. + */ + public shortText( + maxLength: number = 32, + ): string { + if (this.text.length <= maxLength) { + return this.text; + } + + return `${this.text.slice(0, maxLength - 1)}...`; + } +} + +/** + * Tokens that represent a sequence of tokens that does not + * hold an additional meaning in the text. + */ +export class Text extends BaseToken { + public get text(): string { + return BaseToken.render(this.tokens); + } + + constructor( + range: Range, + public readonly tokens: readonly TToken[], + ) { + super(range); + } + + /** + * Create new instance of the token from a provided list of tokens. + * + * @throws if the provided tokens list is empty because this function + * automatically infers the range of the resulting token based + * on the first and last token in the list. + */ + public static fromTokens( + tokens: readonly TToken[], + ): Text { + assert( + tokens.length > 0, + 'Cannot infer range from an empty list of tokens.', + ); + + const range = BaseToken.fullRange(tokens); + + return new Text(range, tokens); + } + + public override toString(): string { + return `text(${this.shortText()})${this.range}`; + } } diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/constants.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/constants.ts new file mode 100644 index 00000000000..0df65a7b88d --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/constants.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NewLine } from '../linesCodec/tokens/newLine.js'; +import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; +import { FormFeed, Space, Tab, VerticalTab } from '../simpleCodec/tokens/index.js'; + +/** + * List of valid "space" tokens that are valid between + * different entities of the Front Matter header. + */ +export const VALID_SPACE_TOKENS = Object.freeze([ + Space, Tab, CarriageReturn, NewLine, FormFeed, VerticalTab, +]); diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts new file mode 100644 index 00000000000..02db7f7fca7 --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VALID_SPACE_TOKENS } from './constants.js'; +import { Word } from '../simpleCodec/tokens/index.js'; +import { TokenStream } from '../utils/tokenStream.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { ReadableStream } from '../../../../base/common/stream.js'; +import { FrontMatterToken, FrontMatterRecord } from './tokens/index.js'; +import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { SimpleDecoder, type TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; +import { PartialFrontMatterRecord, PartialFrontMatterRecordName, PartialFrontMatterRecordNameWithDelimiter } from './parsers/frontMatterRecord.js'; + +/** + * Tokens produced by this decoder. + */ +export type TFrontMatterToken = FrontMatterRecord | TSimpleDecoderToken; + +/** + * Decoder capable of parsing Front Matter contents from a sequence of simple tokens. + */ +export class FrontMatterDecoder extends BaseDecoder { + /** + * Current parser reference responsible for parsing a specific sequence + * of tokens into a standalone token. + */ + private current?: PartialFrontMatterRecordName | PartialFrontMatterRecordNameWithDelimiter | PartialFrontMatterRecord; + + constructor( + stream: ReadableStream | TokenStream, + ) { + if (stream instanceof TokenStream) { + super(stream); + + return; + } + + super(new SimpleDecoder(stream)); + } + + protected override onStreamData(token: TSimpleDecoderToken): void { + if (this.current !== undefined) { + const acceptResult = this.current.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.reEmitCurrentTokens(); + + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + delete this.current; + return; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterToken) { + this._onData.fire(nextParser); + + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + delete this.current; + return; + } + + this.current = nextParser; + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + return; + } + + // a word token starts a new record + if (token instanceof Word) { + this.current = new PartialFrontMatterRecordName(token); + return; + } + + // re-emit all "space" tokens immediately as all of them + // are valid while we are not in the "record parsing" mode + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this._onData.fire(token); + return; + } + } + + // unexpected token type, re-emit existing tokens and continue + this.reEmitCurrentTokens(); + } + + protected override onStreamEnd(): void { + try { + if (this.current === undefined) { + return; + } + + this.reEmitCurrentTokens(); + } finally { + delete this.current; + super.onStreamEnd(); + } + } + + /** + * Re-emit tokens accumulated so far in the current parser object. + */ + protected reEmitCurrentTokens(): void { + if (this.current === undefined) { + return; + } + + for (const token of this.current.tokens) { + this._onData.fire(token); + } + delete this.current; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts new file mode 100644 index 00000000000..22b54f4517f --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VALID_SPACE_TOKENS } from '../constants.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { FrontMatterArray } from '../tokens/frontMatterArray.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Comma, LeftBracket, RightBracket } from '../../simpleCodec/tokens/index.js'; +import { PartialFrontMatterValue, VALID_VALUE_START_TOKENS } from './frontMatterValue.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * List of tokens that can go in-between array items + * and array brackets. + */ +const VALID_DELIMITER_TOKENS = Object.freeze([ + ...VALID_SPACE_TOKENS, + Comma, +]); + +/** + * Responsible for parsing an array syntax (or "inline sequence" + * in YAML terms), e.g. `[1, '2', true, 2.54]` + */ +export class PartialFrontMatterArray extends ParserBase { + /** + * Current parser reference responsible for parsing an array "value". + */ + private currentValueParser?: PartialFrontMatterValue; + + /** + * Whether an array item is allowed in the current position + * of the token sequence. E.g., items are allowed after + * a command or a open bracket, but not immediately after + * another item in the array. + */ + private arrayItemAllowed = true; + + constructor( + private readonly startToken: LeftBracket, + ) { + /** + * Sanity check - logic inside the {@link PartialFrontMatterArray.accept accept} method + * above assumes that the {@link VALID_DELIMITER_TOKENS} tokens list does not intersect + * with the {@link VALID_VALUE_START_TOKENS} tokens list. + * + * Note! the `as` type casting below is ok since we offload the type intersection check + * to the runtime, and is required to avoid compilation errors in Typescript. + */ + for (const DelimiterToken of VALID_DELIMITER_TOKENS) { + for (const ValueStartToken of VALID_VALUE_START_TOKENS as unknown[]) { + assert( + DelimiterToken !== ValueStartToken, + `Delimiter tokens list must not contain value start token '${ValueStartToken}'.`, + ); + } + } + + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + this.currentTokens.push(nextParser); + delete this.currentValueParser; + + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + if (token instanceof RightBracket) { + // sanity check in case this block moves around + // to a different place in the code + assert( + this.currentValueParser === undefined, + `Unexpected end of array. Last value is not finished.`, + ); + + this.currentTokens.push(token); + + this.isConsumed = true; + return { + result: 'success', + nextParser: this.asArrayToken(), + wasTokenConsumed: true, + }; + } + + // iterate until a valid value start token is found + for (const ValidToken of VALID_DELIMITER_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + if ((this.arrayItemAllowed === false) && token instanceof Comma) { + this.arrayItemAllowed = true; + } + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // once we found a valid start value token, create a new value parser + if ((this.arrayItemAllowed === true) && PartialFrontMatterValue.isValueStartToken(token)) { + this.currentValueParser = new PartialFrontMatterValue(); + this.arrayItemAllowed = false; + + return this.accept(token); + } + + // in all other cases fail because of the unexpected token type + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + /** + * Convert current parser into a {@link FrontMatterArray} token, + * if possible. + * + * @throws if the last token in the accumulated token list + * is not a closing bracket ({@link RightBracket}). + */ + public asArrayToken(): FrontMatterArray { + this.isConsumed = true; + const endToken = this.currentTokens[this.currentTokens.length - 1]; + + assertDefined( + endToken, + `No tokens found.`, + ); + + assert( + endToken instanceof RightBracket, + 'Cannot find a closing bracket of the array.', + ); + + const valueTokens: FrontMatterValueToken[] = []; + for (const currentToken of this.currentTokens) { + if (currentToken instanceof FrontMatterValueToken) { + valueTokens.push(currentToken); + } + } + + return new FrontMatterArray([ + this.startToken, + ...valueTokens, + endToken, + ]); + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts new file mode 100644 index 00000000000..6af49acbbce --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts @@ -0,0 +1,277 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../base/common/assert.js'; +import { PartialFrontMatterValue } from './frontMatterValue.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Colon, Word, Dash, Space, Tab } from '../../simpleCodec/tokens/index.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; +import { FrontMatterValueToken, FrontMatterRecordName, type TRecordNameToken, type TRecordSpaceToken, FrontMatterRecordDelimiter, FrontMatterRecord } from '../tokens/index.js'; + +/** + * Tokens that can be used inside a record name. + */ +const VALID_NAME_TOKENS = [ + Word, Dash, +]; + +/** + * List of a "space" tokens that are allowed in between + * record name, delimiter and value tokens inside a record. + * + * E.g. the following is a valid record with `\t` used as a "space" token: + * + * ``` + * \t\tname\t\t:\t\t'value'\t\t\n + * ``` + */ +const VALID_SPACE_TOKENS = [ + Space, Tab, +]; + +/** + * List of tokens that terminate a record name. + */ +const VALID_NAME_STOP_TOKENS = [ + ...VALID_SPACE_TOKENS, + Colon, +]; + +/** + * Parser for a `name` part of a Front Matter record. + * + * E.g., `'name'` in the example below: + * + * ``` + * name: 'value' + * ``` + */ +export class PartialFrontMatterRecordName extends ParserBase { + constructor( + startToken: Word, + ) { + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + for (const ValidToken of VALID_NAME_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // once name is followed by a "space" token or a "colon", we have the full + // record name hence can transition to the next parser + for (const SpaceOrDelimiterToken of VALID_NAME_STOP_TOKENS) { + if (token instanceof SpaceOrDelimiterToken) { + const recordName = new FrontMatterRecordName(this.currentTokens); + + this.isConsumed = true; + return { + result: 'success', + nextParser: new PartialFrontMatterRecordNameWithDelimiter([recordName, token]), + wasTokenConsumed: true, + }; + } + } + + // in all other cases fail due to the unexpected token type for a record name + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * Parser for a record `name` with the `: ` delimiter. + * + * * E.g., `name:` in the example below: + * + * ``` + * name: 'value' + * ``` + */ +export class PartialFrontMatterRecordNameWithDelimiter extends ParserBase { + constructor( + tokens: readonly [FrontMatterRecordName, TRecordSpaceToken | Colon], + ) { + super([...tokens]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + const previousToken = this.currentTokens[this.currentTokens.length - 1]; + + const isSpacingToken = (token instanceof Space) || (token instanceof Tab); + + // delimiter must always be a `:` followed by a "space" character + // once we encounter that sequence, we can transition to the next parser + if ((isSpacingToken === true) && (previousToken instanceof Colon)) { + const recordDelimiter = new FrontMatterRecordDelimiter([ + previousToken, + token, + ]); + + const recordName = this.currentTokens[0]; + + // sanity check + assert( + recordName instanceof FrontMatterRecordName, + `Expected a front matter record name, got '${recordName}'.`, + ); + + this.isConsumed = true; + return { + result: 'success', + nextParser: new PartialFrontMatterRecord( + [recordName, recordDelimiter], + ), + wasTokenConsumed: true, + }; + } + + // allow some spacing before the colon delimiter + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // include the colon delimiter + if (token instanceof Colon) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // otherwise fail due to the unexpected token type between + // record name and record name delimiter tokens + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * Parser for a `record` inside a Front Matter header. + * + * * E.g., `name: 'value'` in the example below: + * + * ``` + * --- + * name: 'value' + * isExample: true + * --- + * ``` + */ +export class PartialFrontMatterRecord extends ParserBase { + constructor( + tokens: [FrontMatterRecordName, FrontMatterRecordDelimiter], + ) { + super(tokens); + } + + /** + * Current parser reference responsible for parsing the "value" part of the record. + */ + private currentValueParser?: PartialFrontMatterValue; + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + this.currentTokens.push(nextParser); + delete this.currentValueParser; + + this.isConsumed = true; + try { + return { + result: 'success', + nextParser: FrontMatterRecord.fromTokens([ + this.currentTokens[0], + this.currentTokens[1], + nextParser, + ]), + wasTokenConsumed, + }; + } catch (_error) { + return { + result: 'failure', + wasTokenConsumed, + }; + } + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + // iterate until the first "value" token is found + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // if token can start a "value" sequence, parse the value + if (PartialFrontMatterValue.isValueStartToken(token)) { + this.currentValueParser = new PartialFrontMatterValue(); + + return this.accept(token); + } + + // otherwise fail due to the unexpected token type for a record value + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts new file mode 100644 index 00000000000..349c26d28cc --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../base/common/assert.js'; +import { SimpleToken } from '../../simpleCodec/tokens/index.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { FrontMatterString, TQuoteToken } from '../tokens/frontMatterString.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * Parser responsible for parsing a string value. + */ +export class PartialFrontMatterString extends ParserBase> { + constructor( + private readonly startToken: TQuoteToken, + ) { + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult> { + this.currentTokens.push(token); + + // iterate until a `matching end quote` is found + if ((token instanceof SimpleToken) && (this.startToken.sameType(token))) { + return { + result: 'success', + nextParser: this.asStringToken(), + wasTokenConsumed: true, + }; + } + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Convert the current parser into a {@link FrontMatterString} token, + * if possible. + * + * @throws if the first and last tokens are not quote tokens of the same type. + */ + public asStringToken(): FrontMatterString { + const endToken = this.currentTokens[this.currentTokens.length - 1]; + + assertDefined( + endToken, + `No matching end token found.`, + ); + + assert( + this.startToken.sameType(endToken), + `String starts with \`${this.startToken.text}\`, but ends with \`${endToken.text}\`.`, + ); + + return new FrontMatterString([ + this.startToken, + ...this.currentTokens + .slice(1, this.currentTokens.length - 1), + endToken, + ]); + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts new file mode 100644 index 00000000000..8e1e32d032a --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { PartialFrontMatterArray } from './frontMatterArray.js'; +import { PartialFrontMatterString } from './frontMatterString.js'; +import { FrontMatterBoolean } from '../tokens/frontMatterBoolean.js'; +import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Word, Quote, DoubleQuote, LeftBracket } from '../../simpleCodec/tokens/index.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * List of tokens that can start a "value" sequence. + * + * - {@link Word} - can be a `boolean` value + * - {@link Quote}, {@link DoubleQuote} - can start a `string` value + * - {@link LeftBracket} - can start an `array` value + */ +export const VALID_VALUE_START_TOKENS = Object.freeze([ + Word, + Quote, + DoubleQuote, + LeftBracket, +]); + +/** + * Type alias for a token that can start a "value" sequence. + */ +type TValueStartToken = InstanceType; + +/** + * Parser responsible for parsing a "value" sequence in a Front Matter header. + */ +export class PartialFrontMatterValue extends ParserBase { + /** + * Current parser reference responsible for parsing + * a specific "value" sequence. + */ + private currentValueParser?: PartialFrontMatterString | PartialFrontMatterArray; + + /** + * Get the tokens that were accumulated so far. + */ + public override get tokens(): readonly TSimpleDecoderToken[] { + if (this.currentValueParser === undefined) { + return []; + } + + return this.currentValueParser.tokens; + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + // current value parser is consumed with its child value parser + this.isConsumed = this.currentValueParser.consumed; + + if (result === 'success') { + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + return { + result: 'success', + nextParser, + wasTokenConsumed, + }; + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + // if the first token represents a `quote` character, try to parse a string value + if ((token instanceof Quote) || (token instanceof DoubleQuote)) { + this.currentValueParser = new PartialFrontMatterString(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // if the first token represents a `[` character, try to parse an array value + if (token instanceof LeftBracket) { + this.currentValueParser = new PartialFrontMatterArray(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // if the first token represents a `word` try to parse a boolean + if (token instanceof Word) { + // in either success or failure case, the parser is consumed + this.isConsumed = true; + + try { + return { + result: 'success', + nextParser: FrontMatterBoolean.fromToken(token), + wasTokenConsumed: true, + }; + } catch (_error) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + } + + // in all other cases fail due to unexpected value sequence + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + /** + * Check if provided token can be a start of a "value" sequence. + * See {@link VALID_VALUE_START_TOKENS} for the list of valid tokens. + */ + public static isValueStartToken( + token: BaseToken, + ): token is TValueStartToken { + for (const ValidToken of VALID_VALUE_START_TOKENS) { + if (token instanceof ValidToken) { + return true; + } + } + + return false; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts new file mode 100644 index 00000000000..b9e54253bcd --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { FrontMatterValueToken, TValueTypeName } from './frontMatterToken.js'; +import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/index.js'; + +/** + * Token that represents an `array` value in a Front Matter header. + */ +export class FrontMatterArray extends FrontMatterValueToken<'array'> { + /** + * Name of the `array` value type. + */ + public override readonly valueTypeName = 'array'; + + constructor( + /** + * List of tokens of the array value. Must start and end + * with square brackets, but tokens in the middle hold + * only the value tokens, omitting commas and spaces. + */ + public readonly tokens: readonly [ + LeftBracket, + ...FrontMatterValueToken[], + RightBracket, + ], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + /** + * List of the array items. + */ + public get items(): readonly FrontMatterValueToken[] { + const result = []; + + for (const token of this.tokens) { + if (token instanceof FrontMatterValueToken) { + result.push(token); + } + } + + return result; + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + public override toString(): string { + return `front-matter-array(${this.shortText()})${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts new file mode 100644 index 00000000000..0203d3c9ab0 --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { Word } from '../../simpleCodec/tokens/index.js'; +import { FrontMatterValueToken } from './frontMatterToken.js'; +import { assertDefined } from '../../../../../base/common/types.js'; + +/** + * Token that represents a `boolean` value in a Front Matter header. + */ +export class FrontMatterBoolean extends FrontMatterValueToken<'boolean'> { + /** + * Name of the `boolean` value type. + */ + public override readonly valueTypeName = 'boolean'; + + constructor( + range: Range, + public readonly value: boolean, + ) { + super(range); + } + + public static fromToken(token: Word): FrontMatterBoolean { + const value = asBoolean(token); + + assertDefined( + value, + `Cannot convert '${token}' to a boolean value.`, + ); + + return new FrontMatterBoolean(token.range, value); + } + + public override get text(): string { + return `${this.value}`; + } + + public override toString(): string { + return `front-matter-boolean(${this.shortText()})${this.range}`; + } +} + +/** + * Try to convert a {@link Word} token to a `boolean` value. + */ +const asBoolean = ( + token: Word, +): boolean | null => { + if (token.text.toLowerCase() === 'true') { + return true; + } + + if (token.text.toLowerCase() === 'false') { + return false; + } + + return null; +}; diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts new file mode 100644 index 00000000000..96ecdeabe43 --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { Colon, Word, Dash, Space, Tab } from '../../simpleCodec/tokens/index.js'; +import { FrontMatterToken, FrontMatterValueToken, TValueTypeName } from '../tokens/frontMatterToken.js'; + +/** + * Type for tokens that can be used inside a record name. + */ +export type TNameToken = Word | Dash; + +/** + * Type for tokens that can be used as "space" in-between record + * name, delimiter and value. + */ +export type TSpaceToken = Space | Tab; + +/** + * Token representing a `record name` inside a Front Matter record. + * + * E.g., `name` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecordName extends FrontMatterToken { + constructor( + public readonly tokens: readonly TNameToken[], + ) { + super(BaseToken.fullRange(tokens)); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-record-name(${this.shortText()})${this.range}`; + } +} + +/** + * Token representing a delimiter of a record inside a Front Matter header. + * + * E.g., `: ` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecordDelimiter extends FrontMatterToken { + constructor( + public readonly tokens: readonly [Colon, TSpaceToken], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-delimiter(${this.shortText()})${this.range}`; + } +} + +/** + * Token representing a `record` inside a Front Matter header. + * + * E.g., `name: 'value'` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecord extends FrontMatterToken { + constructor( + private readonly tokens: readonly [FrontMatterRecordName, FrontMatterRecordDelimiter, FrontMatterValueToken], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + /** + * Token that represent `name` of the record. + * + * E.g., `tools` in the example below: + * + * ``` + * --- + * tools: ['value'] + * --- + * ``` + */ + public get nameToken(): FrontMatterRecordName { + return this.tokens[0]; + } + + /** + * Token that represent `value` of the record. + * + * E.g., `['value']` in the example below: + * + * ``` + * --- + * tools: ['value'] + * --- + * ``` + */ + public get valueToken(): FrontMatterValueToken { + return this.tokens[2]; + } + + /** + * Create new instance from a list of tokens. + * + * @throws if: + * - the list of tokens is not exactly 3 tokens long + * - the first token in the list is not a `FrontMatterRecordName` + * - the second token in the list is not a `FrontMatterRecordDelimiter` + * - the third token in the list is not a `FrontMatterValueToken` + * + */ + public static fromTokens( + tokens: readonly FrontMatterToken[], + ): FrontMatterRecord { + assert( + tokens.length === 3, + `A front matter record must consist of exactly 3 tokens, got '${tokens.length}'.`, + ); + + const token1 = tokens[0]; + const token2 = tokens[1]; + const token3 = tokens[2]; + + assert( + token1 instanceof FrontMatterRecordName, + `Token #1 must be a front matter record name, got '${token1}'.`, + ); + assert( + token2 instanceof FrontMatterRecordDelimiter, + `Token #2 must be a front matter record delimiter, got '${token2}'.`, + ); + assert( + token3 instanceof FrontMatterValueToken, + `Token #3 must be a front matter value, got '${token3}'.`, + ); + + return new FrontMatterRecord([ + token1, token2, token3, + ]); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-record(${this.shortText()})${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts new file mode 100644 index 00000000000..abd368a721d --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { FrontMatterValueToken } from './frontMatterToken.js'; +import { Quote, DoubleQuote } from '../../simpleCodec/tokens/index.js'; + +/** + * Type for any quote token that can be used to wrap a string. + */ +export type TQuoteToken = Quote | DoubleQuote; + +/** + * Token that represents a string value in a Front Matter header. + */ +export class FrontMatterString extends FrontMatterValueToken<'string'> { + /** + * Name of the `string` value type. + */ + public override readonly valueTypeName = 'string'; + + constructor( + public readonly tokens: readonly [TQuote, ...BaseToken[], TQuote], + ) { + super(BaseToken.fullRange(tokens)); + } + + /** + * Text of the string value without the wrapping quotes. + */ + public get cleanText(): string { + return BaseToken.render( + this.tokens.slice(1, this.tokens.length - 1), + ); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-string(${this.shortText()})${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.ts new file mode 100644 index 00000000000..ce9eb28225c --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.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 { BaseToken } from '../../baseToken.js'; + +/** + * Base class for all tokens inside a Front Matter header. + */ +export abstract class FrontMatterToken extends BaseToken { } + +/** + * List of all currently supported value types. + */ +export type TValueTypeName = 'string' | 'boolean' | 'array'; + +/** + * Base class for all tokens that represent a `value` inside a Front Matter header. + */ +export abstract class FrontMatterValueToken extends FrontMatterToken { + /** + * Type name of the `value` represented by this token. + */ + public abstract readonly valueTypeName: TTypeName; +} diff --git a/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts new file mode 100644 index 00000000000..a9959b44726 --- /dev/null +++ b/code/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { FrontMatterArray } from './frontMatterArray.js'; +export { FrontMatterString } from './frontMatterString.js'; +export { FrontMatterBoolean } from './frontMatterBoolean.js'; +export { FrontMatterToken, FrontMatterValueToken } from './frontMatterToken.js'; +export { + FrontMatterRecordName, + FrontMatterRecordDelimiter, + FrontMatterRecord, + type TNameToken as TRecordNameToken, + type TSpaceToken as TRecordSpaceToken, +} from './frontMatterRecord.js'; diff --git a/code/src/vs/editor/common/codecs/linesCodec/linesDecoder.ts b/code/src/vs/editor/common/codecs/linesCodec/linesDecoder.ts index 3bd72e5bd73..67ce2286028 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/linesDecoder.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/linesDecoder.ts @@ -13,9 +13,14 @@ import { assertDefined } from '../../../../base/common/types.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; /** - * Tokens produced by the `LinesDecoder`. + * Any line break token type. */ -export type TLineToken = Line | CarriageReturn | NewLine; +export type TLineBreakToken = CarriageReturn | NewLine; + +/** + * Tokens produced by the {@link LinesDecoder}. + */ +export type TLineToken = Line | TLineBreakToken; /** * The `decoder` part of the `LinesCodec` and is able to transform @@ -53,7 +58,7 @@ export class LinesDecoder extends BaseDecoder { */ private processData( streamEnded: boolean, - ) { + ): void { // iterate over each line of the data buffer, emitting each line // as a `Line` token followed by a `NewLine` token, if applies while (this.buffer.byteLength > 0) { @@ -63,13 +68,17 @@ export class LinesDecoder extends BaseDecoder { : 1; // find the `\r`, `\n`, or `\r\n` tokens in the data - const endOfLineTokens = this.findEndOfLineTokens(lineNumber); - const firstToken = endOfLineTokens[0]; + const endOfLineTokens = this.findEndOfLineTokens( + lineNumber, + streamEnded, + ); + const firstToken: (NewLine | CarriageReturn | undefined) = endOfLineTokens[0]; - // if no end-of-the-line tokens found, stop processing because we - // either (1)need more data to arraive or (2)the stream has ended - // in the case (2) remaining data must be emitted as the last line - if (!firstToken) { + // if no end-of-the-line tokens found, stop the current processing + // attempt because we either (1) need more data to be received or + // (2) the stream has ended; in the case (2) remaining data must + // be emitted as the last line + if (firstToken === undefined) { // (2) if `streamEnded`, we need to emit the whole remaining // data as the last line immediately if (streamEnded) { @@ -88,15 +97,25 @@ export class LinesDecoder extends BaseDecoder { 'No last emitted line found.', ); + // Note! A standalone `\r` token case is not a well-defined case, and + // was primarily used by old Mac OSx systems which treated it as + // a line ending (same as `\n`). Hence for backward compatibility + // with those systems, we treat it as a new line token as well. + // We do that by replacing standalone `\r` token with `\n` one. + if ((endOfLineTokens.length === 1) && (firstToken instanceof CarriageReturn)) { + endOfLineTokens.splice(0, 1, new NewLine(firstToken.range)); + } + // emit the end-of-the-line tokens let startColumn = this.lastEmittedLine.range.endColumn; for (const token of endOfLineTokens) { - const endColumn = startColumn + token.byte.byteLength; + const byteLength = token.byte.byteLength; + const endColumn = startColumn + byteLength; // emit the token updating its column start/end numbers based on // the emitted line text length and previous end-of-the-line token this._onData.fire(token.withRange({ startColumn, endColumn })); // shorten the data buffer by the length of the token - this.buffer = this.buffer.slice(token.byte.byteLength); + this.buffer = this.buffer.slice(byteLength); // update the start column for the next token startColumn = endColumn; } @@ -122,6 +141,7 @@ export class LinesDecoder extends BaseDecoder { */ private findEndOfLineTokens( lineNumber: number, + streamEnded: boolean, ): (CarriageReturn | NewLine)[] { const result = []; @@ -130,7 +150,7 @@ export class LinesDecoder extends BaseDecoder { const newLineIndex = this.buffer.indexOf(NewLine.byte); // if the `\r` comes before the `\n`(if `\n` present at all) - if (carriageReturnIndex >= 0 && (carriageReturnIndex < newLineIndex || newLineIndex === -1)) { + if (carriageReturnIndex >= 0 && ((carriageReturnIndex < newLineIndex) || (newLineIndex === -1))) { // add the carriage return token first result.push( new CarriageReturn(new Range( @@ -154,11 +174,15 @@ export class LinesDecoder extends BaseDecoder { ); } - if (this.buffer.byteLength > carriageReturnIndex + 1) { - // either `\r` or `\r\n` cases found + // either `\r` or `\r\n` cases found; if we have the `\r` token, we can return + // the end-of-line tokens only, if the `\r` is followed by at least one more + // character (it could be a `\n` or any other character), or if the stream has + // ended (which means the `\r` is at the end of the line) + if ((this.buffer.byteLength > carriageReturnIndex + 1) || streamEnded) { return result; } + // in all other cases, return the empty array (no lend-of-line tokens found) return []; } diff --git a/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts b/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts index a509940bc4e..7ac432075b6 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/tokens/carriageReturn.ts @@ -3,21 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Line } from './line.js'; -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { SimpleToken } from '../../simpleCodec/tokens/simpleToken.js'; /** * Token that represent a `carriage return` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class CarriageReturn extends BaseToken { +export class CarriageReturn extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '\r'; + public static override readonly symbol: '\r' = '\r'; /** * The byte representation of the {@link symbol}. @@ -34,33 +31,14 @@ export class CarriageReturn extends BaseToken { /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return CarriageReturn.symbol; } - /** - * Create new `CarriageReturn` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): CarriageReturn { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new CarriageReturn(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ public override toString(): string { - return `carriage-return${this.range}`; + return `CR${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/linesCodec/tokens/line.ts b/code/src/vs/editor/common/codecs/linesCodec/tokens/line.ts index 6669169967f..3159fd6b6f7 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/tokens/line.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/tokens/line.ts @@ -58,6 +58,6 @@ export class Line extends BaseToken { * Returns a string representation of the token. */ public override toString(): string { - return `line("${this.text}")${this.range}`; + return `line("${this.shortText()}")${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts b/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts index fb826b759ca..1211443272d 100644 --- a/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts +++ b/code/src/vs/editor/common/codecs/linesCodec/tokens/newLine.ts @@ -3,21 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Line } from './line.js'; -import { BaseToken } from '../../baseToken.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { Position } from '../../../../../editor/common/core/position.js'; +import { SimpleToken } from '../../simpleCodec/tokens/simpleToken.js'; /** * A token that represent a `new line` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class NewLine extends BaseToken { +export class NewLine extends SimpleToken { /** * The underlying symbol of the `NewLine` token. */ - public static readonly symbol: string = '\n'; + public static override readonly symbol: '\n' = '\n'; /** * The byte representation of the {@link symbol}. @@ -27,7 +24,7 @@ export class NewLine extends BaseToken { /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return NewLine.symbol; } @@ -38,24 +35,6 @@ export class NewLine extends BaseToken { return NewLine.byte; } - /** - * Create new `NewLine` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): NewLine { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new NewLine( - Range.fromPositions(startPosition, endPosition), - ); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts b/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts index bec961719c9..7825ab6ac88 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts @@ -8,22 +8,23 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { LeftBracket } from '../simpleCodec/tokens/brackets.js'; import { PartialMarkdownImage } from './parsers/markdownImage.js'; import { ReadableStream } from '../../../../base/common/stream.js'; +import { TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; import { LeftAngleBracket } from '../simpleCodec/tokens/angleBrackets.js'; import { ExclamationMark } from '../simpleCodec/tokens/exclamationMark.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; -import { SimpleDecoder, TSimpleToken } from '../simpleCodec/simpleDecoder.js'; import { MarkdownCommentStart, PartialMarkdownCommentStart } from './parsers/markdownComment.js'; +import { MarkdownExtensionsDecoder } from '../markdownExtensionsCodec/markdownExtensionsDecoder.js'; import { MarkdownLinkCaption, PartialMarkdownLink, PartialMarkdownLinkCaption } from './parsers/markdownLink.js'; /** - * Tokens handled by this decoder. + * Tokens produced by this decoder. */ -export type TMarkdownToken = MarkdownToken | TSimpleToken; +export type TMarkdownToken = MarkdownToken | TSimpleDecoderToken; /** * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simple tokens. */ -export class MarkdownDecoder extends BaseDecoder { +export class MarkdownDecoder extends BaseDecoder { /** * Current parser object that is responsible for parsing a sequence of tokens into * some markdown entity. Set to `undefined` when no parsing is in progress at the moment. @@ -36,10 +37,10 @@ export class MarkdownDecoder extends BaseDecoder { constructor( stream: ReadableStream, ) { - super(new SimpleDecoder(stream)); + super(new MarkdownExtensionsDecoder(stream)); } - protected override onStreamData(token: TSimpleToken): void { + protected override onStreamData(token: TSimpleDecoderToken): void { // `markdown links` start with `[` character, so here we can // initiate the process of parsing a markdown link if (token instanceof LeftBracket && !this.current) { @@ -92,8 +93,9 @@ export class MarkdownDecoder extends BaseDecoder { // then reset the current parser object for (const token of this.current.tokens) { this._onData.fire(token); - delete this.current; } + + delete this.current; } // if token was not consumed by the parser, call `onStreamData` again @@ -119,11 +121,12 @@ export class MarkdownDecoder extends BaseDecoder { // in all other cases, re-emit existing parser tokens const { tokens } = this.current; - delete this.current; for (const token of [...tokens]) { this._onData.fire(token); } + + delete this.current; } super.onStreamEnd(); diff --git a/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts b/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts index df5f3f028ab..26e78d7f07d 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts @@ -8,7 +8,7 @@ import { Dash } from '../../simpleCodec/tokens/dash.js'; import { pick } from '../../../../../base/common/arrays.js'; import { assert } from '../../../../../base/common/assert.js'; import { MarkdownComment } from '../tokens/markdownComment.js'; -import { TSimpleToken } from '../../simpleCodec/simpleDecoder.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; import { ExclamationMark } from '../../simpleCodec/tokens/exclamationMark.js'; import { LeftAngleBracket, RightAngleBracket } from '../../simpleCodec/tokens/angleBrackets.js'; import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; @@ -16,13 +16,13 @@ import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleC /** * The parser responsible for parsing the ``. If it does, * then the parser transitions to the {@link MarkdownComment} token. */ -export class MarkdownCommentStart extends ParserBase { +export class MarkdownCommentStart extends ParserBase { constructor(tokens: [LeftAngleBracket, ExclamationMark, Dash, Dash]) { super(tokens); } @assertNotConsumed - public accept(token: TSimpleToken): TAcceptTokenResult { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // if received `>` while current token sequence ends with `--`, // then this is the end of the comment sequence if (token instanceof RightAngleBracket && this.endsWithDashes) { @@ -125,7 +125,7 @@ export class MarkdownCommentStart extends ParserBase { +export class PartialMarkdownImage extends ParserBase { /** * Current active parser instance, if in the mode of actively parsing the markdown link sequence. */ @@ -28,7 +28,7 @@ export class PartialMarkdownImage extends ParserBase { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // on the first call we expect a character that begins `markdown link` sequence // hence we initiate the markdown link parsing process, otherwise we fail if (!this.markdownLinkParser) { diff --git a/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts b/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts index e8163286fd2..5ab1d06d5c9 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts @@ -7,8 +7,8 @@ import { MarkdownLink } from '../tokens/markdownLink.js'; import { NewLine } from '../../linesCodec/tokens/newLine.js'; import { assert } from '../../../../../base/common/assert.js'; import { FormFeed } from '../../simpleCodec/tokens/formFeed.js'; -import { TSimpleToken } from '../../simpleCodec/simpleDecoder.js'; import { VerticalTab } from '../../simpleCodec/tokens/verticalTab.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/brackets.js'; import { ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; @@ -26,21 +26,21 @@ const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLin * * The parsing process starts with single `[` token and collects all tokens until * the first `]` token is encountered. In this successful case, the parser transitions - * into the {@linkcode MarkdownLinkCaption} parser type which continues the general + * into the {@link MarkdownLinkCaption} parser type which continues the general * parsing process of the markdown link. * - * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * Otherwise, if one of the stop characters defined in the {@link MARKDOWN_LINK_STOP_CHARACTERS} * is encountered before the `]` token, the parsing process is aborted which is communicated to * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no * longer represent a coherent token entity of a larger size. */ -export class PartialMarkdownLinkCaption extends ParserBase { +export class PartialMarkdownLinkCaption extends ParserBase { constructor(token: LeftBracket) { super([token]); } - public accept(token: TSimpleToken): TAcceptTokenResult { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // any of stop characters is are breaking a markdown link caption sequence if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { return { @@ -70,7 +70,7 @@ export class PartialMarkdownLinkCaption extends ParserBase { - public accept(token: TSimpleToken): TAcceptTokenResult { +export class MarkdownLinkCaption extends ParserBase { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // the `(` character starts the link part of a markdown link // that is the only character that can follow the caption if (token instanceof LeftParenthesis) { @@ -108,10 +108,10 @@ export class MarkdownLinkCaption extends ParserBase { +export class PartialMarkdownLink extends ParserBase { /** * Number of open parenthesis in the sequence. - * See comment in the {@linkcode accept} method for more details. + * See comment in the {@link accept} method for more details. */ private openParensCount: number = 1; constructor( - protected readonly captionTokens: TSimpleToken[], + protected readonly captionTokens: TSimpleDecoderToken[], token: LeftParenthesis, ) { super([token]); } - public override get tokens(): readonly TSimpleToken[] { + public override get tokens(): readonly TSimpleDecoderToken[] { return [...this.captionTokens, ...this.currentTokens]; } - public accept(token: TSimpleToken): TAcceptTokenResult { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // markdown links allow for nested parenthesis inside the link reference part, but // the number of open parenthesis must match the number of closing parenthesis, e.g.: // - `[caption](/some/p()th/file.md)` is a valid markdown link diff --git a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts index f7875d957a2..0181d808a96 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts @@ -51,6 +51,6 @@ export class MarkdownComment extends MarkdownToken { * Returns a string representation of the token. */ public override toString(): string { - return `md-comment("${this.text}")${this.range}`; + return `md-comment("${this.shortText()}")${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts index 75e42e31aaf..5ef293ffaed 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts @@ -136,6 +136,6 @@ export class MarkdownImage extends MarkdownToken { * Returns a string representation of the token. */ public override toString(): string { - return `md-image("${this.text}")${this.range}`; + return `md-image("${this.shortText()}")${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts index a4b15718717..2862e11b358 100644 --- a/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts +++ b/code/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -115,7 +115,7 @@ export class MarkdownLink extends MarkdownToken { const { range } = this; - // note! '+1' for openning `(` of the link + // note! '+1' for opening `(` of the link const startColumn = range.startColumn + this.caption.length + 1; const endColumn = startColumn + this.path.length; @@ -131,6 +131,6 @@ export class MarkdownLink extends MarkdownToken { * Returns a string representation of the token. */ public override toString(): string { - return `md-link("${this.text}")${this.range}`; + return `md-link("${this.shortText()}")${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts new file mode 100644 index 00000000000..e4466e0ee13 --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { ReadableStream } from '../../../../base/common/stream.js'; +import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { MarkdownExtensionsToken } from './tokens/markdownExtensionsToken.js'; +import { SimpleDecoder, TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; +import { PartialFrontMatterHeader, PartialFrontMatterStartMarker } from './parsers/frontMatterHeader.js'; + +/** + * Tokens produced by this decoder. + */ +export type TMarkdownExtensionsToken = MarkdownExtensionsToken | TSimpleDecoderToken; + +/** + * Decoder responsible for decoding extensions of markdown syntax, + * e.g., a `Front Matter` header, etc. + */ +export class MarkdownExtensionsDecoder extends BaseDecoder { + /** + * Current parser object that is responsible for parsing a sequence of tokens into + * some markdown entity. Set to `undefined` when no parsing is in progress at the moment. + */ + private current?: PartialFrontMatterStartMarker | PartialFrontMatterHeader; + + constructor( + stream: ReadableStream, + ) { + super(new SimpleDecoder(stream)); + } + + protected override onStreamData(token: TSimpleDecoderToken): void { + // front matter headers start with a `-` at the first column of the first line + if ((this.current === undefined) && PartialFrontMatterStartMarker.mayStartHeader(token)) { + this.current = new PartialFrontMatterStartMarker(token); + + return; + } + + // if current parser is not initiated, - we are not inside a sequence of tokens + // we care about, therefore re-emit the token immediately and continue + if (this.current === undefined) { + this._onData.fire(token); + return; + } + + // if there is a current parser object, submit the token to it + // so it can progress with parsing the tokens sequence + const parseResult = this.current.accept(token); + if (parseResult.result === 'success') { + const { nextParser } = parseResult; + + // if got a fully parsed out token back, emit it and reset + // the current parser object so a new parsing process can start + if (nextParser instanceof MarkdownExtensionsToken) { + this._onData.fire(nextParser); + delete this.current; + } else { + // otherwise, update the current parser object + this.current = nextParser; + } + } else { + // if failed to parse a sequence of a tokens as a single markdown + // entity (e.g., a link), re-emit the tokens accumulated so far + // then reset the currently initialized parser object + this.reEmitCurrentTokens(); + } + + // if token was not consumed by the parser, call `onStreamData` again + // so the token is properly handled by the decoder in the case when a + // new sequence starts with this token + if (!parseResult.wasTokenConsumed) { + this.onStreamData(token); + } + } + + protected override onStreamEnd(): void { + try { + if (this.current === undefined) { + return; + } + + // if current parser can be converted into a valid Front Matter + // header, then emit it and reset the current parser object + if (this.current instanceof PartialFrontMatterHeader) { + this._onData.fire( + this.current.asFrontMatterHeader(), + ); + delete this.current; + return; + } + + } catch (_error) { + // if failed to convert current parser object to a token, + // re-emit the tokens accumulated so far + this.reEmitCurrentTokens(); + } finally { + delete this.current; + super.onStreamEnd(); + } + } + + /** + * Re-emit tokens accumulated so far in the current parser object. + */ + protected reEmitCurrentTokens(): void { + if (this.current === undefined) { + return; + } + + for (const token of this.current.tokens) { + this._onData.fire(token); + } + delete this.current; + } +} diff --git a/code/src/vs/editor/common/codecs/markdownExtensionsCodec/parsers/frontMatterHeader.ts b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/parsers/frontMatterHeader.ts new file mode 100644 index 00000000000..589aec9d63d --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/parsers/frontMatterHeader.ts @@ -0,0 +1,345 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dash } from '../../simpleCodec/tokens/dash.js'; +import { NewLine } from '../../linesCodec/tokens/newLine.js'; +import { FrontMatterHeader } from '../tokens/frontMatterHeader.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { assert, assertNever } from '../../../../../base/common/assert.js'; +import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; +import { FrontMatterMarker, TMarkerToken } from '../tokens/frontMatterMarker.js'; +import { assertNotConsumed, IAcceptTokenSuccess, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * Parses the start marker of a Front Matter header. + */ +export class PartialFrontMatterStartMarker extends ParserBase { + constructor(token: Dash) { + const { range } = token; + + assert( + range.startLineNumber === 1, + `Front Matter header must start at the first line, but it starts at line #${range.startLineNumber}.`, + ); + + assert( + range.startColumn === 1, + `Front Matter header must start at the beginning of the line, but it starts at ${range.startColumn}.`, + ); + + super([token]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + const previousToken = this.currentTokens[this.currentTokens.length - 1]; + + // collect a sequence of dash tokens that may end with a CR token + if ((token instanceof Dash) || (token instanceof CarriageReturn)) { + // a dash or CR tokens can go only after another dash token + if ((previousToken instanceof Dash) === false) { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + this.currentTokens.push(token); + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } + + // stop collecting dash tokens when a new line token is encountered + if (token instanceof NewLine) { + this.isConsumed = true; + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new PartialFrontMatterHeader( + FrontMatterMarker.fromTokens([ + ...this.currentTokens, + token, + ]), + ), + }; + } + + // any other token is invalid for the `start marker` + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + /** + * Check if provided dash token can be a start of a Front Matter header. + */ + public static mayStartHeader(token: TSimpleDecoderToken): token is Dash { + return (token instanceof Dash) + && (token.range.startLineNumber === 1) + && (token.range.startColumn === 1); + } +} + +/** + * Parses a Front Matter header that already has a start marker + * and possibly some content that follows. + */ +export class PartialFrontMatterHeader extends ParserBase { + /** + * Parser instance for the end marker of the Front Matter header. + */ + private maybeEndMarker?: PartialFrontMatterEndMarker; + + constructor( + public readonly startMarker: FrontMatterMarker, + ) { + super([]); + } + + public override get tokens(): readonly TSimpleDecoderToken[] { + const endMarkerTokens = (this.maybeEndMarker !== undefined) + ? this.maybeEndMarker.tokens + : []; + + return [ + ...this.startMarker.tokens, + ...this.currentTokens, + ...endMarkerTokens, + ]; + } + + /** + * Convert the current token sequence into a {@link FrontMatterHeader} token. + * + * Note! that this method marks the current parser object as "consumed" + * hence it should not be used after this method is called. + */ + public asFrontMatterHeader(): FrontMatterHeader { + assertDefined( + this.maybeEndMarker, + 'Cannot convert to Front Matter header token without an end marker.', + ); + + assert( + this.maybeEndMarker.dashCount === this.startMarker.dashTokens.length, + [ + 'Start and end markers must have the same number of dashes', + `, got ${this.startMarker.dashTokens.length} / ${this.maybeEndMarker.dashCount}.`, + ].join(''), + ); + + this.isConsumed = true; + + return FrontMatterHeader.fromTokens( + this.startMarker.tokens, + this.currentTokens, + this.maybeEndMarker.tokens, + ); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + // if in the mode of parsing the end marker sequence, forward + // the token to the current end marker parser instance + if (this.maybeEndMarker !== undefined) { + return this.acceptEndMarkerToken(token); + } + + // collect all tokens until a `dash token at the beginning of a line` is found + if (((token instanceof Dash) === false) || (token.range.startColumn !== 1)) { + this.currentTokens.push(token); + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } + + // a dash token at the beginning of the line might be a start of the `end marker` + // sequence of the front matter header, hence initialize appropriate parser object + assert( + this.maybeEndMarker === undefined, + `End marker parser must not be present.`, + ); + this.maybeEndMarker = new PartialFrontMatterEndMarker(token); + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } + + /** + * When a end marker parser is present, we pass all tokens to it + * until it is completes the parsing process(either success or failure). + */ + private acceptEndMarkerToken( + token: TSimpleDecoderToken, + ): TAcceptTokenResult { + assertDefined( + this.maybeEndMarker, + `Partial end marker parser must be initialized.`, + ); + + // if we have a partial end marker, we are in the process of parsing + // the end marker, so just pass the token to it and return + const acceptResult = this.maybeEndMarker.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'success') { + const { nextParser } = acceptResult; + const endMarkerParsingComplete = (nextParser instanceof FrontMatterMarker); + + if (endMarkerParsingComplete === false) { + return { + result: 'success', + wasTokenConsumed, + nextParser: this, + }; + } + + const endMarker = nextParser; + + // start and end markers must have the same number of dashes, hence + // if they don't match, we would like to continue parsing the header + // until we find an end marker with the same number of dashes + if (endMarker.dashTokens.length !== this.startMarker.dashTokens.length) { + return this.handleEndMarkerParsingFailure( + endMarker.tokens, + wasTokenConsumed, + ); + } + + this.isConsumed = true; + return { + result: 'success', + wasTokenConsumed: true, + nextParser: FrontMatterHeader.fromTokens( + this.startMarker.tokens, + this.currentTokens, + this.maybeEndMarker.tokens, + ), + }; + } + + // if failed to parse the end marker, we would like to continue parsing + // the header until we find a valid end marker + if (result === 'failure') { + return this.handleEndMarkerParsingFailure( + this.maybeEndMarker.tokens, + wasTokenConsumed, + ); + } + + assertNever( + result, + `Unexpected result '${result}' while parsing the end marker.`, + ); + } + + /** + * On failure to parse the end marker, we need to continue parsing + * the header because there might be another valid end marker in + * the stream of tokens. Therefore we copy over the end marker tokens + * into the list of "content" tokens and reset the end marker parser. + */ + private handleEndMarkerParsingFailure( + tokens: readonly TSimpleDecoderToken[], + wasTokenConsumed: boolean, + ): IAcceptTokenSuccess { + this.currentTokens.push(...tokens); + delete this.maybeEndMarker; + + return { + result: 'success', + wasTokenConsumed, + nextParser: this, + }; + } +} + +/** + * Parser the end marker sequence of a Front Matter header. + */ +class PartialFrontMatterEndMarker extends ParserBase { + constructor(token: Dash) { + const { range } = token; + + assert( + range.startColumn === 1, + `Front Matter header must start at the beginning of the line, but it starts at ${range.startColumn}.`, + ); + + super([token]); + } + + /** + * Number of dashes in the marker. + */ + public get dashCount(): number { + return this.tokens + .filter((token) => { return token instanceof Dash; }) + .length; + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + const previousToken = this.currentTokens[this.currentTokens.length - 1]; + + // collect a sequence of dash tokens that may end with a CR token + if ((token instanceof Dash) || (token instanceof CarriageReturn)) { + // a dash or CR tokens can go only after another dash token + if ((previousToken instanceof Dash) === false) { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + this.currentTokens.push(token); + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: this, + }; + } + + // stop collecting dash tokens when a new line token is encountered + if (token instanceof NewLine) { + this.isConsumed = true; + this.currentTokens.push(token); + + return { + result: 'success', + wasTokenConsumed: true, + nextParser: FrontMatterMarker.fromTokens([ + ...this.currentTokens, + ]), + }; + } + + // any other token is invalid for the `start marker` + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} diff --git a/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.ts b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.ts new file mode 100644 index 00000000000..67efa4d6996 --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { BaseToken, Text } from '../../baseToken.js'; +import { MarkdownExtensionsToken } from './markdownExtensionsToken.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { FrontMatterMarker, TMarkerToken } from './frontMatterMarker.js'; + +/** + * Token that represents a `Front Matter` header in a text. + */ +export class FrontMatterHeader extends MarkdownExtensionsToken { + constructor( + range: Range, + public readonly startMarker: FrontMatterMarker, + public readonly content: Text, + public readonly endMarker: FrontMatterMarker, + ) { + super(range); + } + + /** + * Return complete text representation of the token. + */ + public get text(): string { + const text: string[] = [ + this.startMarker.text, + this.content.text, + this.endMarker.text, + ]; + + return text.join(''); + } + + /** + * Range of the content of the Front Matter header. + */ + public get contentRange(): Range { + return this.content.range; + } + + /** + * Content token of the Front Matter header. + */ + public get contentToken(): Text { + return this.content; + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if (!(other instanceof FrontMatterHeader)) { + return false; + } + + if (this.text.length !== other.text.length) { + return false; + } + + return (this.text === other.text); + } + + /** + * Create new instance of the token from the given tokens. + */ + public static fromTokens( + startMarkerTokens: readonly TMarkerToken[], + contentTokens: readonly TSimpleDecoderToken[], + endMarkerTokens: readonly TMarkerToken[], + ): FrontMatterHeader { + const range = BaseToken.fullRange( + [...startMarkerTokens, ...endMarkerTokens], + ); + + return new FrontMatterHeader( + range, + FrontMatterMarker.fromTokens(startMarkerTokens), + Text.fromTokens(contentTokens), + FrontMatterMarker.fromTokens(endMarkerTokens), + ); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `frontmatter("${this.shortText()}")${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts new file mode 100644 index 00000000000..3c65b1d661b --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { BaseToken } from '../../baseToken.js'; +import { Dash } from '../../simpleCodec/tokens/dash.js'; +import { NewLine } from '../../linesCodec/tokens/newLine.js'; +import { MarkdownExtensionsToken } from './markdownExtensionsToken.js'; +import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; + +/** + * Type for tokens inside a Front Matter header marker. + */ +export type TMarkerToken = Dash | CarriageReturn | NewLine; + +/** + * Marker for the start and end of a Front Matter header. + */ +export class FrontMatterMarker extends MarkdownExtensionsToken { + /** + * Returns complete text representation of the token. + */ + public get text(): string { + return BaseToken.render(this.tokens); + } + + /** + * List of {@link Dash} tokens in the marker. + */ + public get dashTokens(): readonly Dash[] { + return this.tokens + .filter((token) => { return token instanceof Dash; }); + } + + constructor( + range: Range, + public readonly tokens: readonly TMarkerToken[], + ) { + super(range); + } + + /** + * Create new instance of the token from a provided + * list of tokens. + */ + public static fromTokens( + tokens: readonly TMarkerToken[], + ): FrontMatterMarker { + const range = BaseToken.fullRange(tokens); + + return new FrontMatterMarker(range, tokens); + } + + public toString(): string { + return `frontmatter-marker(${this.dashTokens.length}:${this.range})`; + } +} diff --git a/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts new file mode 100644 index 00000000000..82046eb2b4d --- /dev/null +++ b/code/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownToken } from '../../markdownCodec/tokens/markdownToken.js'; + +/** + * Base class for all tokens produced by the `MarkdownExtensionsDecoder`. + */ +export abstract class MarkdownExtensionsToken extends MarkdownToken { } diff --git a/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts b/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts index e088a18f264..c9422f682b5 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/parserBase.ts @@ -53,6 +53,13 @@ export abstract class ParserBase { */ protected isConsumed: boolean = false; + /** + * Whether the parser object was "consumed" hence must not be used anymore. + */ + public get consumed(): boolean { + return this.isConsumed; + } + /** * Number of tokens at the initialization of the current parser. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts b/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts index c32542f28da..6847f9e5671 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts @@ -3,89 +3,113 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Hash } from './tokens/hash.js'; -import { Dash } from './tokens/dash.js'; -import { Colon } from './tokens/colon.js'; -import { FormFeed } from './tokens/formFeed.js'; -import { Tab } from '../simpleCodec/tokens/tab.js'; -import { Word } from '../simpleCodec/tokens/word.js'; -import { VerticalTab } from './tokens/verticalTab.js'; -import { Space } from '../simpleCodec/tokens/space.js'; +import { Line } from '../linesCodec/tokens/line.js'; import { NewLine } from '../linesCodec/tokens/newLine.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { ExclamationMark } from './tokens/exclamationMark.js'; import { ReadableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; -import { LinesDecoder, TLineToken } from '../linesCodec/linesDecoder.js'; -import { LeftBracket, RightBracket, TBracket } from './tokens/brackets.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; -import { LeftParenthesis, RightParenthesis, TParenthesis } from './tokens/parentheses.js'; -import { LeftAngleBracket, RightAngleBracket, TAngleBracket } from './tokens/angleBrackets.js'; +import { LinesDecoder, TLineBreakToken, TLineToken } from '../linesCodec/linesDecoder.js'; +import { + At, + Tab, + Word, + Hash, + Dash, + Colon, + Slash, + Space, + Quote, + Comma, + FormFeed, + DollarSign, + DoubleQuote, + VerticalTab, + type TBracket, + LeftBracket, + RightBracket, + type TCurlyBrace, + LeftCurlyBrace, + RightCurlyBrace, + ExclamationMark, + type TParenthesis, + LeftParenthesis, + RightParenthesis, + type TAngleBracket, + LeftAngleBracket, + RightAngleBracket, +} from './tokens/index.js'; +import { pick } from '../../../../base/common/arrays.js'; +import { ISimpleTokenClass, SimpleToken } from './tokens/simpleToken.js'; /** - * A token type that this decoder can handle. + * Type for all simple tokens. */ -export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed - | CarriageReturn | TBracket | TAngleBracket | TParenthesis - | Colon | Hash | Dash | ExclamationMark; +export type TSimpleToken = Space | Tab | VerticalTab | At | Quote | DoubleQuote + | CarriageReturn | NewLine | FormFeed | TBracket | TAngleBracket | TCurlyBrace + | TParenthesis | Colon | Hash | Dash | ExclamationMark | Slash | DollarSign | Comma + | TLineBreakToken; + +/** +* Type of tokens emitted by this decoder. +*/ +export type TSimpleDecoderToken = TSimpleToken | Word; /** * List of well-known distinct tokens that this decoder emits (excluding * the word stop characters defined below). Everything else is considered - * an arbitrary "text" sequence and is emitted as a single `Word` token. + * an arbitrary "text" sequence and is emitted as a single {@link Word} token. */ -const WELL_KNOWN_TOKENS = Object.freeze([ - Space, Tab, VerticalTab, FormFeed, - LeftBracket, RightBracket, LeftAngleBracket, RightAngleBracket, - LeftParenthesis, RightParenthesis, Colon, Hash, Dash, ExclamationMark, +export const WELL_KNOWN_TOKENS: readonly ISimpleTokenClass[] = Object.freeze([ + LeftParenthesis, RightParenthesis, LeftBracket, RightBracket, LeftCurlyBrace, RightCurlyBrace, + LeftAngleBracket, RightAngleBracket, Space, Tab, VerticalTab, FormFeed, Colon, Hash, Dash, + ExclamationMark, At, Slash, DollarSign, Quote, DoubleQuote, Comma, ]); /** - * Characters that stop a "word" sequence. - * Note! the `\r` and `\n` are excluded from the list because this decoder based on `LinesDecoder` which - * already handles the `carriagereturn`/`newline` cases and emits lines that don't contain them. + * A {@link Word} sequence stops when one of the well-known tokens are encountered. + * Note! the `\r` and `\n` are excluded from the list because this decoder based on + * the {@link LinesDecoder} which emits {@link Line} tokens without them. */ -const WORD_STOP_CHARACTERS: readonly string[] = Object.freeze([ - Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol, - LeftBracket.symbol, RightBracket.symbol, LeftAngleBracket.symbol, RightAngleBracket.symbol, - LeftParenthesis.symbol, RightParenthesis.symbol, - Colon.symbol, Hash.symbol, Dash.symbol, ExclamationMark.symbol, -]); +const WORD_STOP_CHARACTERS: readonly string[] = Object.freeze( + WELL_KNOWN_TOKENS.map(pick('symbol')), +); /** * A decoder that can decode a stream of `Line`s into a stream * of simple token, - `Word`, `Space`, `Tab`, `NewLine`, etc. */ -export class SimpleDecoder extends BaseDecoder { +export class SimpleDecoder extends BaseDecoder { constructor( stream: ReadableStream, ) { super(new LinesDecoder(stream)); } - protected override onStreamData(token: TLineToken): void { + protected override onStreamData(line: TLineToken): void { // re-emit new line tokens immediately - if (token instanceof CarriageReturn || token instanceof NewLine) { - this._onData.fire(token); + if (line instanceof CarriageReturn || line instanceof NewLine) { + this._onData.fire(line); return; } - // loop through the text separating it into `Word` and `Space` tokens + // loop through the text separating it into `Word` and `well-known` tokens + const lineText = line.text.split(''); let i = 0; - while (i < token.text.length) { + while (i < lineText.length) { // index is 0-based, but column numbers are 1-based const columnNumber = i + 1; // check if the current character is a well-known token const tokenConstructor = WELL_KNOWN_TOKENS .find((wellKnownToken) => { - return wellKnownToken.symbol === token.text[i]; + return wellKnownToken.symbol === lineText[i]; }); // if it is a well-known token, emit it and continue to the next one if (tokenConstructor) { - this._onData.fire(tokenConstructor.newOnLine(token, columnNumber)); + this._onData.fire(SimpleToken.newOnLine(line, columnNumber, tokenConstructor)); i++; continue; @@ -95,14 +119,14 @@ export class SimpleDecoder extends BaseDecoder { // that needs to be collected into a single `Word` token, hence // read all the characters until a stop character is encountered let word = ''; - while (i < token.text.length && !(WORD_STOP_CHARACTERS.includes(token.text[i]))) { - word += token.text[i]; + while (i < lineText.length && !(WORD_STOP_CHARACTERS.includes(lineText[i]))) { + word += lineText[i]; i++; } // emit a "text" sequence of characters as a single `Word` token this._onData.fire( - Word.newOnLine(word, token, columnNumber), + Word.newOnLine(word, line, columnNumber), ); } } diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts index 70d264bdd99..52df3621492 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `<` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class LeftAngleBracket extends BaseToken { +export class LeftAngleBracket extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '<'; + public static override readonly symbol: '<' = '<'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return LeftAngleBracket.symbol; } - /** - * Create new `LeftBracket` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): LeftAngleBracket { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new LeftAngleBracket(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ @@ -56,38 +34,19 @@ export class LeftAngleBracket extends BaseToken { * A token that represent a `>` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class RightAngleBracket extends BaseToken { +export class RightAngleBracket extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '>'; + public static override readonly symbol: '>' = '>'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return RightAngleBracket.symbol; } - /** - * Create new `RightAngleBracket` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): RightAngleBracket { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new RightAngleBracket(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/at.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/at.ts new file mode 100644 index 00000000000..7fb98514cc3 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/at.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `@` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class At extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '@' = '@'; + + /** + * Return text representation of the token. + */ + public override get text() { + return At.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `at${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts index 16165cf64a7..f44f0e86b83 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `[` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class LeftBracket extends BaseToken { +export class LeftBracket extends SimpleToken { /** - * The underlying symbol of the `LeftBracket` token. + * The underlying symbol of the token. */ - public static readonly symbol: string = '['; + public static override readonly symbol: '[' = '['; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return LeftBracket.symbol; } - /** - * Create new `LeftBracket` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): LeftBracket { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new LeftBracket(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ @@ -56,38 +34,19 @@ export class LeftBracket extends BaseToken { * A token that represent a `]` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class RightBracket extends BaseToken { +export class RightBracket extends SimpleToken { /** - * The underlying symbol of the `RightBracket` token. + * The underlying symbol of the token. */ - public static readonly symbol: string = ']'; + public static override readonly symbol: ']' = ']'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return RightBracket.symbol; } - /** - * Create new `RightBracket` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): RightBracket { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new RightBracket(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts index 76e9f0cd2b4..6d8be7abad2 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `:` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class Colon extends BaseToken { +export class Colon extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = ':'; + public static override readonly symbol: ':' = ':'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return Colon.symbol; } - /** - * Create new token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): Colon { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new Colon(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts new file mode 100644 index 00000000000..c76ff95b096 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `,` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Comma extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: ',' = ','; + + /** + * Return text representation of the token. + */ + public override get text() { + return Comma.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `comma${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/curlyBraces.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/curlyBraces.ts new file mode 100644 index 00000000000..854d1d2f362 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/curlyBraces.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `{` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftCurlyBrace extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '{' = '{'; + + /** + * Return text representation of the token. + */ + public override get text() { + return LeftCurlyBrace.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-curly-brace${this.range}`; + } +} + +/** + * A token that represent a `}` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightCurlyBrace extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '}' = '}'; + + /** + * Return text representation of the token. + */ + public override get text() { + return RightCurlyBrace.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-curly-brace${this.range}`; + } +} + +/** + * General curly brace token type. + */ +export type TCurlyBrace = LeftCurlyBrace | RightCurlyBrace; diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts index ebc0179eeef..5987fb38cb8 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `-` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class Dash extends BaseToken { +export class Dash extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '-'; + public static override readonly symbol: '-' = '-'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return Dash.symbol; } - /** - * Create new token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): Dash { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new Dash(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/dollarSign.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/dollarSign.ts new file mode 100644 index 00000000000..509bbae0b82 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/dollarSign.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `$` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class DollarSign extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '$' = '$'; + + /** + * Return text representation of the token. + */ + public override get text() { + return DollarSign.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `dollarSign${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts new file mode 100644 index 00000000000..5fc5b4595e8 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `"` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class DoubleQuote extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '"' = '"'; + + /** + * Return text representation of the token. + */ + public override get text() { + return DoubleQuote.symbol; + } + + /** + * Checks if the provided token is of the same type + * as the current one. + */ + public sameType(other: BaseToken): other is typeof this { + return (other instanceof this.constructor); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `double-quote${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts index 025edf70291..8bf6eade73e 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `!` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class ExclamationMark extends BaseToken { +export class ExclamationMark extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '!'; + public static override readonly symbol: '!' = '!'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return ExclamationMark.symbol; } - /** - * Create new token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): ExclamationMark { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new ExclamationMark(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts index 35f55dd8a2a..efdf6ec0048 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/formFeed.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { Position } from '../../../../../editor/common/core/position.js'; +import { SimpleToken } from './simpleToken.js'; /** * Token that represent a `form feed` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class FormFeed extends BaseToken { +export class FormFeed extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '\f'; + public static override readonly symbol: '\f' = '\f'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return FormFeed.symbol; } - /** - * Create new `FormFeed` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): FormFeed { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new FormFeed(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts index ddca12a2279..ac8cd96bb3c 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `#` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class Hash extends BaseToken { +export class Hash extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '#'; + public static override readonly symbol: '#' = '#'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return Hash.symbol; } - /** - * Create new token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): Hash { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new Hash(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts new file mode 100644 index 00000000000..b8e38bc3325 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { At } from './at.js'; +export { Tab } from './tab.js'; +export { Dash } from './dash.js'; +export { Hash } from './hash.js'; +export { Word } from './word.js'; +export { Colon } from './colon.js'; +export { Quote } from './quote.js'; +export { Slash } from './slash.js'; +export { Space } from './space.js'; +export { Comma } from './comma.js'; +export { FormFeed } from './formFeed.js'; +export { DollarSign } from './dollarSign.js'; +export { VerticalTab } from './verticalTab.js'; +export { SimpleToken } from './simpleToken.js'; +export { DoubleQuote } from './doubleQuote.js'; +export { ExclamationMark } from './exclamationMark.js'; +export { type TBracket, LeftBracket, RightBracket } from './brackets.js'; +export { type TCurlyBrace, LeftCurlyBrace, RightCurlyBrace } from './curlyBraces.js'; +export { type TParenthesis, LeftParenthesis, RightParenthesis } from './parentheses.js'; +export { type TAngleBracket, LeftAngleBracket, RightAngleBracket } from './angleBrackets.js'; diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts index d3509824f53..ec7b07d0e03 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Range } from '../../../core/range.js'; -import { Position } from '../../../core/position.js'; -import { Line } from '../../linesCodec/tokens/line.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `(` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class LeftParenthesis extends BaseToken { +export class LeftParenthesis extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '('; + public static override readonly symbol: '(' = '('; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return LeftParenthesis.symbol; } - /** - * Create new `LeftParenthesis` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): LeftParenthesis { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new LeftParenthesis(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ @@ -56,38 +34,19 @@ export class LeftParenthesis extends BaseToken { * A token that represent a `)` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class RightParenthesis extends BaseToken { +export class RightParenthesis extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = ')'; + public static override readonly symbol: ')' = ')'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return RightParenthesis.symbol; } - /** - * Create new `RightParenthesis` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): RightParenthesis { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new RightParenthesis(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts new file mode 100644 index 00000000000..85c331b0be8 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `'` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Quote extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '\'' = '\''; + + /** + * Return text representation of the token. + */ + public override get text() { + return Quote.symbol; + } + + /** + * Checks if the provided token is of the same type + * as the current one. + */ + public sameType(other: BaseToken): other is Quote { + return (other instanceof this.constructor); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `quote${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/simpleToken.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/simpleToken.ts new file mode 100644 index 00000000000..07a3f987d00 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/simpleToken.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { BaseToken } from '../../baseToken.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * Interface for a class that can be instantiated into a {@link SimpleToken}. + */ +export interface ISimpleTokenClass { + /** + * Character representing the token. + */ + readonly symbol: string; + + /** + * Constructor for the token. + */ + new(...args: any[]): TSimpleToken; +} + +/** + * Base class for all "simple" tokens with a `range`. + * A simple token is the one that represents a single character. + */ +export abstract class SimpleToken extends BaseToken { + /** + * The underlying symbol of the token. + */ + public static readonly symbol: string; + + /** + * Return text representation of the token. + */ + public abstract override get text(): string; + + /** + * Create new token instance with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + Constructor: ISimpleTokenClass, + ): SimpleToken { + const { range } = line; + + return new Constructor(new Range( + range.startLineNumber, + atColumnNumber, + range.startLineNumber, + atColumnNumber + Constructor.symbol.length, + )); + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/slash.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/slash.ts new file mode 100644 index 00000000000..a0c6dee88a0 --- /dev/null +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/slash.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `/` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Slash extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '/' = '/'; + + /** + * Return text representation of the token. + */ + public override get text() { + return Slash.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `slash${this.range}`; + } +} diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts index 18a5dff4a0a..66f17e49a25 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/space.ts @@ -3,47 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { Position } from '../../../../../editor/common/core/position.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `space` with a `range`. The `range` * value reflects the position of the token in the original data. - */ -export class Space extends BaseToken { + */export class Space extends SimpleToken { /** * The underlying symbol of the `Space` token. */ - public static readonly symbol: string = ' '; + public static override readonly symbol: ' ' = ' '; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return Space.symbol; } - /** - * Create new `Space` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): Space { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new Space(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts index c0d775ff8cd..58d72627db6 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { Position } from '../../../../../editor/common/core/position.js'; +import { SimpleToken } from './simpleToken.js'; /** * A token that represent a `tab` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class Tab extends BaseToken { +export class Tab extends SimpleToken { /** * The underlying symbol of the token. */ - public static readonly symbol: string = '\t'; + public static override readonly symbol: '\t' = '\t'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return Tab.symbol; } - /** - * Create new token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): Tab { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new Tab(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts index c6b87db0e37..7afd67db343 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/verticalTab.ts @@ -3,47 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { Position } from '../../../../../editor/common/core/position.js'; +import { SimpleToken } from './simpleToken.js'; /** * Token that represent a `vertical tab` with a `range`. The `range` * value reflects the position of the token in the original data. */ -export class VerticalTab extends BaseToken { +export class VerticalTab extends SimpleToken { /** * The underlying symbol of the `VerticalTab` token. */ - public static readonly symbol: string = '\v'; + public static override readonly symbol: '\v' = '\v'; /** * Return text representation of the token. */ - public get text(): string { + public override get text() { return VerticalTab.symbol; } - /** - * Create new `VerticalTab` token with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine( - line: Line, - atColumnNumber: number, - ): VerticalTab { - const { range } = line; - - const startPosition = new Position(range.startLineNumber, atColumnNumber); - const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); - - return new VerticalTab(Range.fromPositions( - startPosition, - endPosition, - )); - } - /** * Returns a string representation of the token. */ diff --git a/code/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts b/code/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts index 2ca5598ac4b..c7fd146103a 100644 --- a/code/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts +++ b/code/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts @@ -67,6 +67,6 @@ export class Word extends BaseToken { * Returns a string representation of the token. */ public override toString(): string { - return `word("${this.text}")${this.range}`; + return `word("${this.shortText()}")${this.range}`; } } diff --git a/code/src/vs/editor/common/codecs/utils/tokenStream.ts b/code/src/vs/editor/common/codecs/utils/tokenStream.ts new file mode 100644 index 00000000000..d5d0c5f0967 --- /dev/null +++ b/code/src/vs/editor/common/codecs/utils/tokenStream.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../baseToken.js'; +import { assert, assertNever } from '../../../../base/common/assert.js'; +import { ObservableDisposable } from '../../../../base/common/observableDisposable.js'; +import { newWriteableStream, WriteableStream, ReadableStream } from '../../../../base/common/stream.js'; + +/** + * A readable stream of provided tokens. + */ +export class TokenStream extends ObservableDisposable implements ReadableStream { + /** + * Underlying writable stream instance. + */ + private readonly stream: WriteableStream; + + /** + * Index of the next token to be sent. + */ + private index: number; + + /** + * Interval reference that is used to periodically send + * tokens to the stream in the background. + */ + private interval: ReturnType | undefined; + + /** + * Number of tokens left to be sent. + */ + private get tokensLeft(): number { + return this.tokens.length - this.index; + } + + constructor( + private readonly tokens: readonly T[], + ) { + super(); + + this.stream = newWriteableStream(null); + this.index = 0; + + // send couple of tokens immediately + this.sendTokens(); + } + + /** + * Start periodically sending tokens to the stream + * asynchronously in the background. + */ + public startStream(): this { + // already running, noop + if (this.interval !== undefined) { + return this; + } + + // no tokens to send, end the stream immediately + if (this.tokens.length === 0) { + this.stream.end(); + return this; + } + + // periodically send tokens to the stream + this.interval = setInterval(() => { + if (this.tokensLeft === 0) { + clearInterval(this.interval); + delete this.interval; + + return; + } + + this.sendTokens(); + }, 1); + + return this; + } + + /** + * Stop tokens sending interval. + */ + public stopStream(): this { + if (this.interval === undefined) { + return this; + } + + clearInterval(this.interval); + delete this.interval; + + return this; + } + + /** + * Sends a provided number of tokens to the stream. + */ + private sendTokens( + tokensCount: number = 25, + ): void { + if (this.tokensLeft <= 0) { + return; + } + + // send up to 10 tokens at a time + let tokensToSend = Math.min(this.tokensLeft, tokensCount); + while (tokensToSend > 0) { + assert( + this.index < this.tokens.length, + `Token index '${this.index}' is out of bounds.`, + ); + + this.stream.write(this.tokens[this.index]); + this.index++; + tokensToSend--; + } + + // if sent all tokens, end the stream immediately + if (this.tokensLeft === 0) { + this.stream.end(); + } + } + + public pause(): void { + this.stopStream(); + + return this.stream.pause(); + } + + public resume(): void { + this.startStream(); + + return this.stream.resume(); + } + + public destroy(): void { + this.dispose(); + } + + public removeListener(event: string, callback: Function): void { + return this.stream.removeListener(event, callback); + } + + public on(event: 'data', callback: (data: T) => void): void; + public on(event: 'error', callback: (err: Error) => void): void; + public on(event: 'end', callback: () => void): void; + public on(event: 'data' | 'error' | 'end', callback: (arg?: any) => void): void { + if (event === 'data') { + this.stream.on(event, callback); + // this is the convention of the readable stream, - when + // the `data` event is registered, the stream is started + this.startStream(); + + return; + } + + if (event === 'error') { + return this.stream.on(event, callback); + } + + if (event === 'end') { + return this.stream.on(event, callback); + } + + assertNever( + event, + `Unexpected event name '${event}'.`, + ); + } + + /** + * Cleanup send interval and destroy the stream. + */ + public override dispose(): void { + this.stopStream(); + this.stream.destroy(); + + super.dispose(); + } +} diff --git a/code/src/vs/editor/common/config/editorOptions.ts b/code/src/vs/editor/common/config/editorOptions.ts index d418872f4bf..bb4eee89030 100644 --- a/code/src/vs/editor/common/config/editorOptions.ts +++ b/code/src/vs/editor/common/config/editorOptions.ts @@ -986,6 +986,7 @@ export interface IEnvironmentalOptions { readonly inputMode: 'insert' | 'overtype'; readonly accessibilitySupport: AccessibilitySupport; readonly glyphMarginDecorationLaneCount: number; + readonly editContextSupported: boolean; } /** @@ -1943,8 +1944,7 @@ class EffectiveExperimentalEditContextEnabled extends ComputedEditorOption 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/code/src/vs/editor/common/core/offsetEdit.ts b/code/src/vs/editor/common/core/offsetEdit.ts index f35eb12d5b4..adcbe4da86d 100644 --- a/code/src/vs/editor/common/core/offsetEdit.ts +++ b/code/src/vs/editor/common/core/offsetEdit.ts @@ -11,6 +11,17 @@ import { OffsetRange } from './offsetRange.js'; * Use `TextEdit` to describe edits for a 1-based line/column text. */ export class OffsetEdit { + public static join(edits: readonly OffsetEdit[]): OffsetEdit { + if (edits.length === 0) { + return OffsetEdit.empty; + } + let result = edits[0]; + for (let i = 1; i < edits.length; i++) { + result = result.compose(edits[i]); + } + return result; + } + public static readonly empty = new OffsetEdit([]); public static fromJson(data: IOffsetEdit): OffsetEdit { diff --git a/code/src/vs/editor/common/core/offsetRange.ts b/code/src/vs/editor/common/core/offsetRange.ts index 7e1aa41b29e..4786748450d 100644 --- a/code/src/vs/editor/common/core/offsetRange.ts +++ b/code/src/vs/editor/common/core/offsetRange.ts @@ -202,6 +202,10 @@ export class OffsetRange implements IOffsetRange { export class OffsetRangeSet { private readonly _sortedRanges: OffsetRange[] = []; + public get ranges(): OffsetRange[] { + return [...this._sortedRanges]; + } + public addRange(range: OffsetRange): void { let i = 0; while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive < range.start) { diff --git a/code/src/vs/editor/common/core/positionToOffset.ts b/code/src/vs/editor/common/core/positionToOffset.ts index 3548c8d0e89..dd8d81a2cbe 100644 --- a/code/src/vs/editor/common/core/positionToOffset.ts +++ b/code/src/vs/editor/common/core/positionToOffset.ts @@ -4,16 +4,59 @@ *--------------------------------------------------------------------------------------------*/ import { findLastIdxMonotonous } from '../../../base/common/arraysFind.js'; +import { ITextModel } from '../model.js'; +import { OffsetEdit, SingleOffsetEdit } from './offsetEdit.js'; import { OffsetRange } from './offsetRange.js'; import { Position } from './position.js'; import { Range } from './range.js'; +import { SingleTextEdit, TextEdit } from './textEdit.js'; import { TextLength } from './textLength.js'; -export class PositionOffsetTransformer { +export abstract class PositionOffsetTransformerBase { + abstract getOffset(position: Position): number; + + getOffsetRange(range: Range): OffsetRange { + return new OffsetRange( + this.getOffset(range.getStartPosition()), + this.getOffset(range.getEndPosition()) + ); + } + + abstract getPosition(offset: number): Position; + + getRange(offsetRange: OffsetRange): Range { + return Range.fromPositions( + this.getPosition(offsetRange.start), + this.getPosition(offsetRange.endExclusive) + ); + } + + getOffsetEdit(edit: TextEdit): OffsetEdit { + const edits = edit.edits.map(e => this.getSingleOffsetEdit(e)); + return new OffsetEdit(edits); + } + + getSingleOffsetEdit(edit: SingleTextEdit): SingleOffsetEdit { + return new SingleOffsetEdit(this.getOffsetRange(edit.range), edit.text); + } + + getSingleTextEdit(edit: SingleOffsetEdit): SingleTextEdit { + return new SingleTextEdit(this.getRange(edit.replaceRange), edit.newText); + } + + getTextEdit(edit: OffsetEdit): TextEdit { + const edits = edit.edits.map(e => this.getSingleTextEdit(e)); + return new TextEdit(edits); + } +} + +export class PositionOffsetTransformer extends PositionOffsetTransformerBase { private readonly lineStartOffsetByLineIdx: number[]; private readonly lineEndOffsetByLineIdx: number[]; constructor(public readonly text: string) { + super(); + this.lineStartOffsetByLineIdx = []; this.lineEndOffsetByLineIdx = []; @@ -31,7 +74,7 @@ export class PositionOffsetTransformer { this.lineEndOffsetByLineIdx.push(text.length); } - getOffset(position: Position): number { + override getOffset(position: Position): number { const valPos = this._validatePosition(position); return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1; } @@ -55,27 +98,13 @@ export class PositionOffsetTransformer { return position; } - getOffsetRange(range: Range): OffsetRange { - return new OffsetRange( - this.getOffset(range.getStartPosition()), - this.getOffset(range.getEndPosition()) - ); - } - - getPosition(offset: number): Position { + override getPosition(offset: number): Position { const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset); const lineNumber = idx + 1; const column = offset - this.lineStartOffsetByLineIdx[idx] + 1; return new Position(lineNumber, column); } - getRange(offsetRange: OffsetRange): Range { - return Range.fromPositions( - this.getPosition(offsetRange.start), - this.getPosition(offsetRange.endExclusive) - ); - } - getTextLength(offsetRange: OffsetRange): TextLength { return TextLength.ofRange(this.getRange(offsetRange)); } @@ -89,3 +118,21 @@ export class PositionOffsetTransformer { return this.lineEndOffsetByLineIdx[lineNumber - 1] - this.lineStartOffsetByLineIdx[lineNumber - 1]; } } + +export function getPositionOffsetTransformerFromTextModel(textModel: ITextModel): PositionOffsetTransformerBase { + return new PositionOffsetTransformerWithTextModel(textModel); +} + +class PositionOffsetTransformerWithTextModel extends PositionOffsetTransformerBase { + constructor(private readonly _textModel: ITextModel) { + super(); + } + + override getOffset(position: Position): number { + return this._textModel.getOffsetAt(position); + } + + override getPosition(offset: number): Position { + return this._textModel.getPositionAt(offset); + } +} diff --git a/code/src/vs/editor/common/core/rangeSingleLine.ts b/code/src/vs/editor/common/core/rangeSingleLine.ts new file mode 100644 index 00000000000..d16cf24b0dd --- /dev/null +++ b/code/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/code/src/vs/editor/common/core/textEdit.ts b/code/src/vs/editor/common/core/textEdit.ts index 83baa3bff43..80d03aa1e34 100644 --- a/code/src/vs/editor/common/core/textEdit.ts +++ b/code/src/vs/editor/common/core/textEdit.ts @@ -193,6 +193,68 @@ export class TextEdit { equals(other: TextEdit): boolean { return equals(this.edits, other.edits, (a, b) => a.equals(b)); } + + toString(text: AbstractText | string | undefined): string { + if (text === undefined) { + return this.edits.map(edit => edit.toString()).join('\n'); + } + + if (typeof text === 'string') { + return this.toString(new StringText(text)); + } + + if (this.edits.length === 0) { + return ''; + } + + return this.edits.map(edit => { + const maxLength = 10; + const originalText = text.getValueOfRange(edit.range); + + // Get text before the edit + const beforeRange = Range.fromPositions( + new Position(Math.max(1, edit.range.startLineNumber - 1), 1), + edit.range.getStartPosition() + ); + let beforeText = text.getValueOfRange(beforeRange); + if (beforeText.length > maxLength) { + beforeText = '...' + beforeText.substring(beforeText.length - maxLength); + } + + // Get text after the edit + const afterRange = Range.fromPositions( + edit.range.getEndPosition(), + new Position(edit.range.endLineNumber + 1, 1) + ); + let afterText = text.getValueOfRange(afterRange); + if (afterText.length > maxLength) { + afterText = afterText.substring(0, maxLength) + '...'; + } + + // Format the replaced text + let replacedText = originalText; + if (replacedText.length > maxLength) { + const halfMax = Math.floor(maxLength / 2); + replacedText = replacedText.substring(0, halfMax) + '...' + + replacedText.substring(replacedText.length - halfMax); + } + + // Format the new text + let newText = edit.text; + if (newText.length > maxLength) { + const halfMax = Math.floor(maxLength / 2); + newText = newText.substring(0, halfMax) + '...' + + newText.substring(newText.length - halfMax); + } + + if (replacedText.length === 0) { + // allow-any-unicode-next-line + return `${beforeText}❰${newText}❱${afterText}`; + } + // allow-any-unicode-next-line + return `${beforeText}❰${replacedText}↦${newText}❱${afterText}`; + }).join('\n'); + } } export class SingleTextEdit { diff --git a/code/src/vs/editor/common/editorCommon.ts b/code/src/vs/editor/common/editorCommon.ts index e25edb4fb50..9cf2cd59a83 100644 --- a/code/src/vs/editor/common/editorCommon.ts +++ b/code/src/vs/editor/common/editorCommon.ts @@ -634,6 +634,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; fontSize?: string; + lineHeight?: number; textDecoration?: string; cursor?: string; color?: string | ThemeColor; diff --git a/code/src/vs/editor/common/languages.ts b/code/src/vs/editor/common/languages.ts index 0edb100dfea..39ffd5dd032 100644 --- a/code/src/vs/editor/common/languages.ts +++ b/code/src/vs/editor/common/languages.ts @@ -847,6 +847,8 @@ export interface InlineCompletion { readonly showRange?: IRange; readonly warning?: InlineCompletionWarning; + + readonly displayLocation?: InlineCompletionDisplayLocation; } export interface InlineCompletionWarning { @@ -854,6 +856,11 @@ export interface InlineCompletionWarning { icon?: IconPath; } +export interface InlineCompletionDisplayLocation { + range: IRange; + label: string; +} + /** * TODO: add `| URI | { light: URI; dark: URI }`. */ @@ -897,13 +904,24 @@ export interface InlineCompletionsProvider): void; + /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ freeInlineCompletions(completions: T): void; + onDidChangeInlineCompletions?: Event; + /** * Only used for {@link yieldsToGroupIds}. * Multiple providers can have the same group id. @@ -923,6 +941,22 @@ export interface InlineCompletionsProvider = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; // User did an explicit action to accept +} | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; // User did an explicit action to reject +} | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: TInlineCompletion; + userTypingDisagreed: boolean; +}; + export interface CodeAction { title: string; command?: Command; @@ -2059,7 +2093,7 @@ export interface CommentThread { onDidChangeInitialCollapsibleState: Event; state?: CommentThreadState; applicability?: CommentThreadApplicability; - canReply: boolean; + canReply: boolean | CommentAuthorInformation; input?: CommentInput; onDidChangeInput: Event; onDidChangeLabel: Event; diff --git a/code/src/vs/editor/common/languages/highlights/css.scm b/code/src/vs/editor/common/languages/highlights/css.scm index 6fb6e5c7e45..1dccbce2796 100644 --- a/code/src/vs/editor/common/languages/highlights/css.scm +++ b/code/src/vs/editor/common/languages/highlights/css.scm @@ -13,6 +13,11 @@ "*=" ] @keyword.operator.css +[ + "+" + ">" +] @keyword.operator.combinator.css + (comment) @comment.block.css ; Selectors @@ -21,10 +26,19 @@ (class_selector) @entity.other.attribute-name.class.css +(id_selector) @entity.other.attribute-name.id.css + (tag_name) @entity.name.tag.css +(universal_selector) @entity.name.tag.wildcard.css + (pseudo_class_selector) @entity.other.attribute-name.pseudo-class.css +(pseudo_element_selector + "::" @entity.other.attribute-name.pseudo-element.css + . + (tag_name) @entity.other.attribute-name.pseudo-element.css) + (attribute_name) @entity.other.attribute-name.css ; @ Rules @@ -36,6 +50,7 @@ ("@media") ("@supports") ("@keyframes") + (at_keyword) ] @keyword.control.at-rule.css (keyword_query) @support.constant.media.css @@ -81,6 +96,11 @@ ((color_value) @constant.other.color.rgb-value.hex.css (#match? @constant.other.color.rgb-value.hex.css "^#.*")) +(call_expression + (function_name) @meta.function.variable.css (#eq? @meta.function.variable.css "var") + (arguments + (plain_value) @variable.argument.css)) + ; Special Functions (call_expression @@ -88,3 +108,7 @@ (#eq? @support.function.url.css "url")) (arguments (plain_value) @variable.parameter.url.css)) + +; Keywords + +(important) @keyword.other.important.css diff --git a/code/src/vs/editor/common/languages/highlights/regex.scm b/code/src/vs/editor/common/languages/highlights/regex.scm index 27b29813943..af098dba6c3 100644 --- a/code/src/vs/editor/common/languages/highlights/regex.scm +++ b/code/src/vs/editor/common/languages/highlights/regex.scm @@ -21,10 +21,29 @@ "]" ] @punctuation.definition.character-class.regexp -[ - "(?:" - "(?<" -] @punctuation.definition.group.assertion.regexp +( + ([ + "(?<" + ] @punctuation.definition.group.assertion.regexp) + . + [ + "=" + "!" + ] @punctuation.definition.group.assertion.regexp +) @meta.assertion.look-behind.regexp + +( + ([ + "(?" + ] @punctuation.definition.group.assertion.regexp) + . + [ + "=" + "!" + ] @punctuation.definition.group.assertion.regexp +) @meta.assertion.look-ahead.regexp + +"(?:" @punctuation.definition.group.regexp @punctuation.definition.group.no-capture.regexp (lookaround_assertion ("!") @punctuation.definition.group.assertion.regexp) @@ -43,6 +62,10 @@ (boundary_assertion) ] @keyword.control.anchor.regexp +(class_character) @constant.character-class.regexp + +(identity_escape) @constant.character.escape.regexp + [ ((identity_escape) @internal.regexp (#match? @internal.regexp "\\[^ux]")) ] @constant.character.escape.regexp @@ -67,10 +90,19 @@ (pattern_character) @constant.character.numeric.regexp ) @constant.character.numeric.regexp -[ - (character_class_escape) - (control_escape) -] @constant.other.character-class.regexp +( + ((identity_escape) @internal.regexp (#eq? @internal.regexp "\\x")) + . + (class_character) @constant.character.numeric.regexp + . + (class_character) @constant.character.numeric.regexp +) @constant.character.numeric.regexp + +(control_escape) @constant.other.character-class.regexp + +(character_class_escape) @constant.character.escape.regexp + +(decimal_escape) @keyword.other.back-reference.regexp ("|") @keyword.operator.or.regexp @@ -92,5 +124,3 @@ "^" @keyword.operator.negation.regexp (class_range "-" @constant.other.character-class.range.regexp) ]) - -(class_character) @constant.character-class.regexp diff --git a/code/src/vs/editor/common/languages/highlights/typescript.scm b/code/src/vs/editor/common/languages/highlights/typescript.scm index 860ef7027a1..c1c7d8f0df1 100644 --- a/code/src/vs/editor/common/languages/highlights/typescript.scm +++ b/code/src/vs/editor/common/languages/highlights/typescript.scm @@ -28,6 +28,8 @@ (template_literal_type) ] @string.template.ts) +(template_substitution) @meta.template.expression.ts + (string . ([ "\"" @@ -130,6 +132,9 @@ (arrow_function parameter: (identifier) @variable.parameter.ts) +(type_predicate + name: (identifier) @variable.parameter.ts) + ; Function and method calls (call_expression @@ -238,6 +243,7 @@ (unary_expression ([ "-" + "+" ]) @keyword.operator.arithmetic.ts) [ diff --git a/code/src/vs/editor/common/languages/modesRegistry.ts b/code/src/vs/editor/common/languages/modesRegistry.ts index 28fdbf80051..a61af971493 100644 --- a/code/src/vs/editor/common/languages/modesRegistry.ts +++ b/code/src/vs/editor/common/languages/modesRegistry.ts @@ -7,7 +7,7 @@ import * as nls from '../../../nls.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { ILanguageExtensionPoint } from './language.js'; import { Registry } from '../../../platform/registry/common/platform.js'; -import { IDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import { Mimes } from '../../../base/common/mime.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../platform/configuration/common/configurationRegistry.js'; @@ -16,14 +16,15 @@ export const Extensions = { ModesRegistry: 'editor.modesRegistry' }; -export class EditorModesRegistry { +export class EditorModesRegistry extends Disposable { private readonly _languages: ILanguageExtensionPoint[]; - private readonly _onDidChangeLanguages = new Emitter(); + private readonly _onDidChangeLanguages = this._register(new Emitter()); public readonly onDidChangeLanguages: Event = this._onDidChangeLanguages.event; constructor() { + super(); this._languages = []; } diff --git a/code/src/vs/editor/common/model.ts b/code/src/vs/editor/common/model.ts index 146c9830e04..5134e5e4d93 100644 --- a/code/src/vs/editor/common/model.ts +++ b/code/src/vs/editor/common/model.ts @@ -19,7 +19,7 @@ import { IWordAtPosition } from './core/wordHelper.js'; import { FormattingOptions } from './languages.js'; import { ILanguageSelection } from './languages/language.js'; import { IBracketPairsTextModelPart } from './textModelBracketPairs.js'; -import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from './textModelEvents.js'; +import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; import { IGuidesTextModelPart } from './textModelGuides.js'; import { ITokenizationTextModelPart } from './tokenizationTextModelPart.js'; import { UndoRedoGroup } from '../../platform/undoRedo/common/undoRedo.js'; @@ -218,6 +218,10 @@ export interface IModelDecorationOptions { * with the specified {@link IModelDecorationGlyphMarginOptions} in the glyph margin. */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; + /** + * If set, the decoration will override the line height of the lines it spans. + */ + lineHeight?: number | null; /** * If set, the decoration will be rendered in the lines decorations with this CSS class name. */ @@ -1108,6 +1112,12 @@ export interface ITextModel { */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** * @internal */ @@ -1238,6 +1248,14 @@ export interface ITextModel { * @event */ readonly onDidChangeDecorations: Event; + /** + * An event emitted when line heights from decorations changes. + * This event is emitted only when adding, removing or changing a decoration + * and not when doing edits in the model (i.e. when decoration ranges change) + * @internal + * @event + */ + readonly onDidChangeLineHeight: Event; /** * An event emitted when the model options have changed. * @event diff --git a/code/src/vs/editor/common/model/textModel.ts b/code/src/vs/editor/common/model/textModel.ts index c17724ca9ad..0ea09949e81 100644 --- a/code/src/vs/editor/common/model/textModel.ts +++ b/code/src/vs/editor/common/model/textModel.ts @@ -40,13 +40,14 @@ import { SearchParams, TextModelSearch } from './textModelSearch.js'; import { TokenizationTextModelPart } from './tokenizationTextModelPart.js'; import { AttachedViews } from './tokens.js'; import { IBracketPairsTextModelPart } from '../textModelBracketPairs.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted, ModelLineHeightChangedEvent, ModelLineHeightChanged } from '../textModelEvents.js'; import { IGuidesTextModelPart } from '../textModelGuides.js'; import { ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { IColorTheme } from '../../../platform/theme/common/themeService.js'; import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js'; import { TokenArray } from '../tokens/tokenArray.js'; +import { SetWithKey } from '../../../base/common/collections.js'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -213,7 +214,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onWillDispose: Emitter = this._register(new Emitter()); public readonly onWillDispose: Event = this._onWillDispose.event; - private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter(affectedInjectedTextLines => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines))); + private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter((affectedInjectedTextLines, affectedLineHeights) => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines, affectedLineHeights))); public readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; public get onDidChangeLanguage() { return this._tokenizationTextModelPart.onDidChangeLanguage; } @@ -228,6 +229,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onDidChangeInjectedText: Emitter = this._register(new Emitter()); + private readonly _onDidChangeLineHeight: Emitter = this._register(new Emitter()); + public readonly onDidChangeLineHeight: Event = this._onDidChangeLineHeight.event; + private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter()); public onDidChangeContent(listener: (e: IModelContentChangedEvent) => void): IDisposable { return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); @@ -413,6 +417,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati || this._onDidChangeOptions.hasListeners() || this._onDidChangeAttached.hasListeners() || this._onDidChangeInjectedText.hasListeners() + || this._onDidChangeLineHeight.hasListeners() || this._eventEmitter.hasListeners() ); } @@ -1576,17 +1581,19 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati //#region Decorations - private handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines: Set | null): void { + private handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines: Set | null, affectedLineHeights: Set | null): void { // This is called before the decoration changed event is fired. - if (affectedInjectedTextLines === null || affectedInjectedTextLines.size === 0) { - return; + if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { + const affectedLines = Array.from(affectedInjectedTextLines); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); + } + if (affectedLineHeights && affectedLineHeights.size > 0) { + const affectedLines = Array.from(affectedLineHeights); + const lineHeightChangeEvent = affectedLines.map(specialLineHeightChange => new ModelLineHeightChanged(specialLineHeightChange.ownerId, specialLineHeightChange.decorationId, specialLineHeightChange.lineNumber, specialLineHeightChange.lineHeight)); + this._onDidChangeLineHeight.fire(new ModelLineHeightChangedEvent(lineHeightChangeEvent)); } - - const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); - - this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); } public changeDecorations(callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T | null { @@ -1606,10 +1613,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._changeDecorationImpl(id, newRange); + this._changeDecorationImpl(ownerId, id, newRange); }, changeDecorationOptions: (id: string, options: model.IModelDecorationOptions) => { - this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); + this._changeDecorationOptionsImpl(ownerId, id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { this._deltaDecorationsImpl(ownerId, [id], []); @@ -1761,6 +1768,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._decorationsTree.getAllInjectedText(this, ownerId); } + public getCustomLineHeightsDecorations(ownerId: number = 0): model.IModelDecoration[] { + return this._decorationsTree.getAllCustomLineHeights(this, ownerId); + } + private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { const startOffset = this._buffer.getOffsetAt(lineNumber, 1); const endOffset = startOffset + this._buffer.getLineLength(lineNumber); @@ -1789,7 +1800,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._buffer.getRangeAt(start, end - start); } - private _changeDecorationImpl(decorationId: string, _range: IRange): void { + private _changeDecorationImpl(ownerId: number, decorationId: string, _range: IRange): void { const node = this._decorations[decorationId]; if (!node) { return; @@ -1803,6 +1814,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const oldRange = this.getDecorationRange(decorationId); this._onDidChangeDecorations.recordLineAffectedByInjectedText(oldRange!.startLineNumber); } + if (node.options.lineHeight !== null) { + const oldRange = this.getDecorationRange(decorationId); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, oldRange!.startLineNumber, null); + } const range = this._validateRangeRelaxedNoAllocations(_range); const startOffset = this._buffer.getOffsetAt(range.startLineNumber, range.startColumn); @@ -1819,9 +1834,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (node.options.before) { this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber); } + if (node.options.lineHeight !== null) { + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, range.startLineNumber, node.options.lineHeight); + } } - private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { + private _changeDecorationOptionsImpl(ownerId: number, decorationId: string, options: ModelDecorationOptions): void { const node = this._decorations[decorationId]; if (!node) { return; @@ -1841,6 +1859,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const nodeRange = this._decorationsTree.getNodeRange(this, node); this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber); } + if (node.options.lineHeight !== null || options.lineHeight !== null) { + const nodeRange = this._decorationsTree.getNodeRange(this, node); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, nodeRange.startLineNumber, options.lineHeight); + } const movedInOverviewRuler = nodeWasInOverviewRuler !== nodeIsInOverviewRuler; const changedWhetherInjectedText = isOptionsInjectedText(options) !== isNodeInjectedText(node); @@ -1871,8 +1893,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (oldDecorationIndex < oldDecorationsLen) { // (1) get ourselves an old node + let decorationId: string; do { - node = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; + decorationId = oldDecorationsIds[oldDecorationIndex++]; + node = this._decorations[decorationId]; } while (!node && oldDecorationIndex < oldDecorationsLen); // (2) remove the node from the tree (if it exists) @@ -1885,7 +1909,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const nodeRange = this._decorationsTree.getNodeRange(this, node); this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber); } - + if (node.options.lineHeight !== null) { + const nodeRange = this._decorationsTree.getNodeRange(this, node); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, nodeRange.startLineNumber, null); + } this._decorationsTree.delete(node); if (!suppressEvents) { @@ -1920,7 +1947,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (node.options.before) { this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber); } - + if (node.options.lineHeight !== null) { + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, node.id, range.startLineNumber, node.options.lineHeight); + } if (!suppressEvents) { this._onDidChangeDecorations.checkAffectedAndFire(options); } @@ -2090,6 +2119,12 @@ class DecorationsTrees { return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } + public getAllCustomLineHeights(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] { + const versionId = host.getVersionId(); + const result = this._search(filterOwnerId, false, false, versionId, false); + return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); + } + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId, onlyMarginDecorations); @@ -2316,6 +2351,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly hoverMessage: IMarkdownString | IMarkdownString[] | null; readonly glyphMarginHoverMessage: IMarkdownString | IMarkdownString[] | null; readonly isWholeLine: boolean; + readonly lineHeight: number | null; readonly showIfCollapsed: boolean; readonly collapseOnReplaceEdit: boolean; readonly overviewRuler: ModelDecorationOverviewRulerOptions | null; @@ -2351,6 +2387,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || null; this.lineNumberHoverMessage = options.lineNumberHoverMessage || null; this.isWholeLine = options.isWholeLine || false; + this.lineHeight = options.lineHeight ?? null; this.showIfCollapsed = options.showIfCollapsed || false; this.collapseOnReplaceEdit = options.collapseOnReplaceEdit || false; this.overviewRuler = options.overviewRuler ? new ModelDecorationOverviewRulerOptions(options.overviewRuler) : null; @@ -2391,6 +2428,20 @@ function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorat return ModelDecorationOptions.createDynamic(options); } +class LineHeightChangingDecoration { + + public static toKey(obj: LineHeightChangingDecoration): string { + return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; + } + + constructor( + public readonly ownerId: number, + public readonly decorationId: string, + public readonly lineNumber: number, + public readonly lineHeight: number | null + ) { } +} + class DidChangeDecorationsEmitter extends Disposable { private readonly _actual: Emitter = this._register(new Emitter()); @@ -2401,10 +2452,11 @@ class DidChangeDecorationsEmitter extends Disposable { private _affectsMinimap: boolean; private _affectsOverviewRuler: boolean; private _affectedInjectedTextLines: Set | null = null; + private _affectedLineHeights: SetWithKey | null = null; private _affectsGlyphMargin: boolean; private _affectsLineNumber: boolean; - constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null) => void) { + constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null, affectedLineHeights: SetWithKey | null) => void) { super(); this._deferredCnt = 0; this._shouldFireDeferred = false; @@ -2431,6 +2483,8 @@ class DidChangeDecorationsEmitter extends Disposable { this._affectedInjectedTextLines?.clear(); this._affectedInjectedTextLines = null; + this._affectedLineHeights?.clear(); + this._affectedLineHeights = null; } } @@ -2441,6 +2495,13 @@ class DidChangeDecorationsEmitter extends Disposable { this._affectedInjectedTextLines.add(lineNumber); } + public recordLineAffectedByLineHeightChange(ownerId: number, decorationId: string, lineNumber: number, lineHeight: number | null): void { + if (!this._affectedLineHeights) { + this._affectedLineHeights = new SetWithKey([], LineHeightChangingDecoration.toKey); + } + this._affectedLineHeights.add(new LineHeightChangingDecoration(ownerId, decorationId, lineNumber, lineHeight)); + } + public checkAffectedAndFire(options: ModelDecorationOptions): void { this._affectsMinimap ||= !!options.minimap?.position; this._affectsOverviewRuler ||= !!options.overviewRuler?.color; @@ -2465,7 +2526,7 @@ class DidChangeDecorationsEmitter extends Disposable { } private doFire() { - this.handleBeforeFire(this._affectedInjectedTextLines); + this.handleBeforeFire(this._affectedInjectedTextLines, this._affectedLineHeights); const event: IModelDecorationsChangedEvent = { affectsMinimap: this._affectsMinimap, diff --git a/code/src/vs/editor/common/model/tokenizationTextModelPart.ts b/code/src/vs/editor/common/model/tokenizationTextModelPart.ts index 84dea7fdba6..8b1ea211b93 100644 --- a/code/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/code/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -211,7 +211,7 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz // #region Semantic Tokens public setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void { - this._semanticTokens.set(tokens, isComplete); + this._semanticTokens.set(tokens, isComplete, this._textModel); this._emitModelTokensChangedEvent({ semanticTokensApplied: tokens !== null, diff --git a/code/src/vs/editor/common/services/textResourceConfigurationService.ts b/code/src/vs/editor/common/services/textResourceConfigurationService.ts index 04420d7f5e6..85ef41882c4 100644 --- a/code/src/vs/editor/common/services/textResourceConfigurationService.ts +++ b/code/src/vs/editor/common/services/textResourceConfigurationService.ts @@ -110,7 +110,7 @@ export class TextResourceConfigurationService extends Disposable implements ITex return true; } if (overrideIdentifier) { - //TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/240410 + //TODO@sandy081 workaround for https://github.com/microsoft/vscode/issues/240410 return configurationChangeEvent.affectedKeys.has(`[${overrideIdentifier}]`); } return false; diff --git a/code/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts b/code/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts index 5c7116a39e7..bf8744a3878 100644 --- a/code/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts +++ b/code/src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts @@ -5,7 +5,7 @@ import type * as Parser from '@vscode/tree-sitter-wasm'; import { ITreeSitterParseResult, ITextModelTreeSitter, RangeChange, TreeParseUpdateEvent, ITreeSitterImporter, ModelTreeUpdateEvent } from '../treeSitterParserService.js'; -import { Disposable, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; import { ITextModel } from '../../model.js'; import { IModelContentChange, IModelContentChangedEvent } from '../../textModelEvents.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -40,7 +40,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit private _query: Parser.Query | undefined; // TODO: @alexr00 use a better data structure for this - private _injectionTreeSitterTrees: Map = new Map(); + private readonly _injectionTreeSitterTrees: DisposableMap = this._register(new DisposableMap()); private _versionId: number = 0; get parseResult(): ITreeSitterParseResult | undefined { return this._rootTreeSitterTree; } @@ -278,6 +278,12 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit parentLanguage: string, modelChanges: IModelContentChangedEvent[] | undefined ): Promise { + if (injections.size === 0) { + this._injectionTreeSitterTrees.clearAndDisposeAll(); + return; + } + + const unseenInjections: Set = new Set(this._injectionTreeSitterTrees.keys()); for (const [languageId, ranges] of injections) { const language = await this._treeSitterLanguages.getLanguage(languageId); if (!language) { @@ -286,9 +292,13 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit const treeSitterTree = await this._getOrCreateInjectedTree(languageId, language, parentTree, parentLanguage); if (treeSitterTree) { + unseenInjections.delete(languageId); this._onDidChangeContent(treeSitterTree, modelChanges, ranges); } } + for (const unseenInjection of unseenInjections) { + this._injectionTreeSitterTrees.deleteAndDispose(unseenInjection); + } } private async _getOrCreateInjectedTree( diff --git a/code/src/vs/editor/common/services/treeSitter/treeSitterLanguages.ts b/code/src/vs/editor/common/services/treeSitter/treeSitterLanguages.ts index 322873229ae..d7ec9cff2d5 100644 --- a/code/src/vs/editor/common/services/treeSitter/treeSitterLanguages.ts +++ b/code/src/vs/editor/common/services/treeSitter/treeSitterLanguages.ts @@ -5,13 +5,14 @@ import type * as Parser from '@vscode/tree-sitter-wasm'; import { AppResourcePath, FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from '../../../../base/common/network.js'; -import { ITreeSitterImporter } from '../treeSitterParserService.js'; +import { EDITOR_EXPERIMENTAL_PREFER_TREESITTER, ITreeSitterImporter } from '../treeSitterParserService.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { canASAR } from '../../../../amdX.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { PromiseResult } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; export const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; @@ -30,9 +31,21 @@ export class TreeSitterLanguages extends Disposable { constructor(private readonly _treeSitterImporter: ITreeSitterImporter, private readonly _fileService: IFileService, private readonly _environmentService: IEnvironmentService, + configurationService: IConfigurationService, private readonly _registeredLanguages: Map, ) { super(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(EDITOR_EXPERIMENTAL_PREFER_TREESITTER)) { + for (const language of this._languages.keys()) { + if (e.affectsConfiguration(`${EDITOR_EXPERIMENTAL_PREFER_TREESITTER}.${language}`)) { + if (this._languages.getSyncIfCached(language) === undefined) { + this._languages.delete(language); + } + } + } + } + })); } public getOrInitLanguage(languageId: string): Parser.Language | undefined { @@ -105,6 +118,14 @@ class AsyncCache { isCached(key: TKey): boolean { return this._values.get(key)?.result !== undefined; } + + delete(key: TKey) { + return this._values.delete(key); + } + + keys() { + return this._values.keys(); + } } class PromiseWithSyncAccess { diff --git a/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 174f31549d5..b46816ec5e8 100644 --- a/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/code/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -41,7 +41,7 @@ export class TreeSitterTextModelService extends Disposable implements ITreeSitte @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); - this._treeSitterLanguages = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService, this._environmentService, this._registeredLanguages)); + this._treeSitterLanguages = this._register(new TreeSitterLanguages(this._treeSitterImporter, fileService, this._environmentService, this._configurationService, this._registeredLanguages)); this.onDidAddLanguage = this._treeSitterLanguages.onDidAddLanguage; this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(EDITOR_EXPERIMENTAL_PREFER_TREESITTER)) { diff --git a/code/src/vs/editor/common/standalone/standaloneEnums.ts b/code/src/vs/editor/common/standalone/standaloneEnums.ts index 9316d4f99de..593e5194573 100644 --- a/code/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/code/src/vs/editor/common/standalone/standaloneEnums.ts @@ -420,6 +420,12 @@ export enum InlayHintKind { Parameter = 2 } +export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2 +} + /** * How an {@link InlineCompletionsProvider inline completion provider} was triggered. */ diff --git a/code/src/vs/editor/common/textModelEvents.ts b/code/src/vs/editor/common/textModelEvents.ts index c35d0472106..3123b120b28 100644 --- a/code/src/vs/editor/common/textModelEvents.ts +++ b/code/src/vs/editor/common/textModelEvents.ts @@ -234,6 +234,37 @@ export class ModelRawLineChanged { } } + +/** + * An event describing that a line height has changed in the model. + * @internal + */ +export class ModelLineHeightChanged { + /** + * Editor owner ID + */ + public readonly ownerId: number; + /** + * The decoration ID that has changed. + */ + public readonly decorationId: string; + /** + * The line that has changed. + */ + public readonly lineNumber: number; + /** + * The line height on the line. + */ + public readonly lineHeight: number | null; + + constructor(ownerId: number, decorationId: string, lineNumber: number, lineHeight: number | null) { + this.ownerId = ownerId; + this.decorationId = decorationId; + this.lineNumber = lineNumber; + this.lineHeight = lineHeight; + } +} + /** * An event describing that line(s) have been deleted in a model. * @internal @@ -361,6 +392,19 @@ export class ModelInjectedTextChangedEvent { } } +/** + * An event describing a change of a line height. + * @internal + */ +export class ModelLineHeightChangedEvent { + + public readonly changes: ModelLineHeightChanged[]; + + constructor(changes: ModelLineHeightChanged[]) { + this.changes = changes; + } +} + /** * @internal */ diff --git a/code/src/vs/editor/common/tokens/common.ts b/code/src/vs/editor/common/tokens/common.ts new file mode 100644 index 00000000000..d8b3a9c76b4 --- /dev/null +++ b/code/src/vs/editor/common/tokens/common.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class RateLimiter { + private _lastRun: number; + private readonly _minimumTimeBetweenRuns: number; + + constructor(public readonly timesPerSecond: number = 5) { + this._lastRun = 0; + this._minimumTimeBetweenRuns = 1000 / timesPerSecond; + } + + public runIfNotLimited(callback: () => void): void { + const now = Date.now(); + if (now - this._lastRun >= this._minimumTimeBetweenRuns) { + this._lastRun = now; + callback(); + } + } +} diff --git a/code/src/vs/editor/common/tokens/contiguousTokensEditing.ts b/code/src/vs/editor/common/tokens/contiguousTokensEditing.ts index 28d444a2df0..7da48f7527b 100644 --- a/code/src/vs/editor/common/tokens/contiguousTokensEditing.ts +++ b/code/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/code/src/vs/editor/common/tokens/lineTokens.ts b/code/src/vs/editor/common/tokens/lineTokens.ts index ebe43f09b96..1ebab1927d7 100644 --- a/code/src/vs/editor/common/tokens/lineTokens.ts +++ b/code/src/vs/editor/common/tokens/lineTokens.ts @@ -112,6 +112,10 @@ export class LineTokens implements IViewLineTokens { this.languageIdCodec = decoder; } + public getTextLength(): number { + return this._text.length; + } + public equals(other: IViewLineTokens): boolean { if (other instanceof LineTokens) { return this.slicedEquals(other, 0, this._tokensCount); diff --git a/code/src/vs/editor/common/tokens/sparseMultilineTokens.ts b/code/src/vs/editor/common/tokens/sparseMultilineTokens.ts index 50887a06af6..931e55eb3cc 100644 --- a/code/src/vs/editor/common/tokens/sparseMultilineTokens.ts +++ b/code/src/vs/editor/common/tokens/sparseMultilineTokens.ts @@ -7,6 +7,8 @@ import { CharCode } from '../../../base/common/charCode.js'; import { Position } from '../core/position.js'; import { IRange, Range } from '../core/range.js'; import { countEOL } from '../core/eolCounter.js'; +import { ITextModel } from '../model.js'; +import { RateLimiter } from './common.js'; /** * Represents sparse tokens over a contiguous range of lines. @@ -162,6 +164,10 @@ export class SparseMultilineTokens { this._tokens.acceptInsertText(lineIndex, position.column - 1, eolCount, firstLineLength, lastLineLength, firstCharCode); } + + public reportIfInvalid(model: ITextModel): void { + this._tokens.reportIfInvalid(model, this._startLineNumber); + } } class SparseMultilineTokensStorage { @@ -558,6 +564,26 @@ class SparseMultilineTokensStorage { tokens[offset + 2] = tokenEndCharacter; } } + + private static _rateLimiter = new RateLimiter(10 / 60); // limit to 10 times per minute + + public reportIfInvalid(model: ITextModel, startLineNumber: number): void { + for (let i = 0; i < this._tokenCount; i++) { + const lineNumber = this._getDeltaLine(i) + startLineNumber; + + if (lineNumber > model.getLineCount()) { + SparseMultilineTokensStorage._rateLimiter.runIfNotLimited(() => { + console.error('Invalid Semantic Tokens Data From Extension: lineNumber > model.getLineCount()'); + }); + } + + if (this._getEndCharacter(i) > model.getLineLength(lineNumber)) { + SparseMultilineTokensStorage._rateLimiter.runIfNotLimited(() => { + console.error('Invalid Semantic Tokens Data From Extension: end character > model.getLineLength(lineNumber)'); + }); + } + } + } } export class SparseLineTokens { diff --git a/code/src/vs/editor/common/tokens/sparseTokensStore.ts b/code/src/vs/editor/common/tokens/sparseTokensStore.ts index dd89936c989..69d217f6036 100644 --- a/code/src/vs/editor/common/tokens/sparseTokensStore.ts +++ b/code/src/vs/editor/common/tokens/sparseTokensStore.ts @@ -9,6 +9,7 @@ import { LineTokens } from './lineTokens.js'; import { SparseMultilineTokens } from './sparseMultilineTokens.js'; import { ILanguageIdCodec } from '../languages.js'; import { MetadataConsts } from '../encodedTokenAttributes.js'; +import { ITextModel } from '../model.js'; /** * Represents sparse tokens in a text model. @@ -34,9 +35,15 @@ export class SparseTokensStore { return (this._pieces.length === 0); } - public set(pieces: SparseMultilineTokens[] | null, isComplete: boolean): void { + public set(pieces: SparseMultilineTokens[] | null, isComplete: boolean, textModel: ITextModel | undefined = undefined): void { this._pieces = pieces || []; this._isComplete = isComplete; + + if (textModel) { + for (const p of this._pieces) { + p.reportIfInvalid(textModel); + } + } } public setPartial(_range: Range, pieces: SparseMultilineTokens[]): Range { @@ -124,7 +131,7 @@ export class SparseTokensStore { } public addSparseTokens(lineNumber: number, aTokens: LineTokens): LineTokens { - if (aTokens.getLineContent().length === 0) { + if (aTokens.getTextLength() === 0) { // Don't do anything for empty lines return aTokens; } @@ -160,8 +167,10 @@ export class SparseTokensStore { }; for (let bIndex = 0; bIndex < bLen; bIndex++) { - const bStartCharacter = bTokens.getStartCharacter(bIndex); - const bEndCharacter = bTokens.getEndCharacter(bIndex); + // bTokens is not validated yet, but aTokens is. We want to make sure that the LineTokens we return + // are valid, so we clamp the ranges to ensure that. + const bStartCharacter = Math.min(bTokens.getStartCharacter(bIndex), aTokens.getTextLength()); + const bEndCharacter = Math.min(bTokens.getEndCharacter(bIndex), aTokens.getTextLength()); const bMetadata = bTokens.getMetadata(bIndex); const bMask = ( diff --git a/code/src/vs/editor/common/tokens/tokenArray.ts b/code/src/vs/editor/common/tokens/tokenArray.ts index bc089d1604a..555e5654dcc 100644 --- a/code/src/vs/editor/common/tokens/tokenArray.ts +++ b/code/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/code/src/vs/editor/common/tokens/tokenWithTextArray.ts b/code/src/vs/editor/common/tokens/tokenWithTextArray.ts new file mode 100644 index 00000000000..765d480a12b --- /dev/null +++ b/code/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/code/src/vs/editor/common/viewLayout/lineHeights.ts b/code/src/vs/editor/common/viewLayout/lineHeights.ts new file mode 100644 index 00000000000..37996f9f967 --- /dev/null +++ b/code/src/vs/editor/common/viewLayout/lineHeights.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { binarySearch2 } from '../../../base/common/arrays.js'; +import { intersection } from '../../../base/common/collections.js'; + +export class CustomLine { + + public index: number; + public lineNumber: number; + public specialHeight: number; + public prefixSum: number; + public maximumSpecialHeight: number; + public decorationId: string; + public deleted: boolean; + + constructor(decorationId: string, index: number, lineNumber: number, specialHeight: number, prefixSum: number) { + this.decorationId = decorationId; + this.index = index; + this.lineNumber = lineNumber; + this.specialHeight = specialHeight; + this.prefixSum = prefixSum; + this.maximumSpecialHeight = specialHeight; + this.deleted = false; + } +} + +/** + * Manages line heights in the editor with support for custom line heights from decorations. + * + * This class maintains an ordered collection of line heights, where each line can have either + * the default height or a custom height specified by decorations. It supports efficient querying + * of individual line heights as well as accumulated heights up to a specific line. + * + * Line heights are stored in a sorted array for efficient binary search operations. Each line + * with custom height is represented by a {@link CustomLine} object which tracks its special height, + * accumulated height prefix sum, and associated decoration ID. + * + * The class optimizes performance by: + * - Using binary search to locate lines in the ordered array + * - Batching updates through a pending changes mechanism + * - Computing prefix sums for O(1) accumulated height lookup + * - Tracking maximum height for lines with multiple decorations + * - Efficiently handling document changes (line insertions and deletions) + * + * When lines are inserted or deleted, the manager updates line numbers and prefix sums + * for all affected lines. It also handles special cases like decorations that span + * the insertion/deletion points by re-applying those decorations appropriately. + * + * All query operations automatically commit pending changes to ensure consistent results. + * Clients can modify line heights by adding or removing custom line height decorations, + * which are tracked by their unique decoration IDs. + */ +export class LineHeightsManager { + + private _decorationIDToCustomLine: ArrayMap = new ArrayMap(); + private _orderedCustomLines: CustomLine[] = []; + private _pendingSpecialLinesToInsert: CustomLine[] = []; + private _invalidIndex: number = 0; + private _defaultLineHeight: number; + private _hasPending: boolean = false; + + constructor(defaultLineHeight: number, customLineHeightData: ICustomLineHeightData[]) { + this._defaultLineHeight = defaultLineHeight; + if (customLineHeightData.length > 0) { + for (const data of customLineHeightData) { + this.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight); + } + this.commit(); + } + } + + set defaultLineHeight(defaultLineHeight: number) { + this._defaultLineHeight = defaultLineHeight; + } + + get defaultLineHeight() { + return this._defaultLineHeight; + } + + public removeCustomLineHeight(decorationID: string): void { + const customLines = this._decorationIDToCustomLine.get(decorationID); + if (!customLines) { + return; + } + this._decorationIDToCustomLine.delete(decorationID); + for (const customLine of customLines) { + customLine.deleted = true; + this._invalidIndex = Math.min(this._invalidIndex, customLine.index); + } + this._hasPending = true; + } + + public insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void { + this.removeCustomLineHeight(decorationId); + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const customLine = new CustomLine(decorationId, -1, lineNumber, lineHeight, 0); + this._pendingSpecialLinesToInsert.push(customLine); + } + this._hasPending = true; + } + + public heightForLineNumber(lineNumber: number): number { + const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber); + if (searchIndex >= 0) { + return this._orderedCustomLines[searchIndex].maximumSpecialHeight; + } + return this._defaultLineHeight; + } + + public getAccumulatedLineHeightsIncludingLineNumber(lineNumber: number): number { + const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber); + if (searchIndex >= 0) { + return this._orderedCustomLines[searchIndex].prefixSum + this._orderedCustomLines[searchIndex].maximumSpecialHeight; + } + if (searchIndex === -1) { + return this._defaultLineHeight * lineNumber; + } + const modifiedIndex = -(searchIndex + 1); + const previousSpecialLine = this._orderedCustomLines[modifiedIndex - 1]; + return previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (lineNumber - previousSpecialLine.lineNumber); + } + + public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { + const deleteCount = toLineNumber - fromLineNumber + 1; + const numberOfCustomLines = this._orderedCustomLines.length; + const candidateStartIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); + let startIndexOfDeletion: number; + if (candidateStartIndexOfDeletion >= 0) { + startIndexOfDeletion = candidateStartIndexOfDeletion; + for (let i = candidateStartIndexOfDeletion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + startIndexOfDeletion--; + } else { + break; + } + } + } else { + startIndexOfDeletion = candidateStartIndexOfDeletion === -(numberOfCustomLines + 1) && candidateStartIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateStartIndexOfDeletion + 1); + } + const candidateEndIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(toLineNumber); + let endIndexOfDeletion: number; + if (candidateEndIndexOfDeletion >= 0) { + endIndexOfDeletion = candidateEndIndexOfDeletion; + for (let i = candidateEndIndexOfDeletion + 1; i < numberOfCustomLines; i++) { + if (this._orderedCustomLines[i].lineNumber === toLineNumber) { + endIndexOfDeletion++; + } else { + break; + } + } + } else { + endIndexOfDeletion = candidateEndIndexOfDeletion === -(numberOfCustomLines + 1) && candidateEndIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateEndIndexOfDeletion + 1); + } + const isEndIndexBiggerThanStartIndex = endIndexOfDeletion > startIndexOfDeletion; + const isEndIndexEqualToStartIndexAndCoversCustomLine = endIndexOfDeletion === startIndexOfDeletion + && this._orderedCustomLines[startIndexOfDeletion] + && this._orderedCustomLines[startIndexOfDeletion].lineNumber >= fromLineNumber + && this._orderedCustomLines[startIndexOfDeletion].lineNumber <= toLineNumber; + + if (isEndIndexBiggerThanStartIndex || isEndIndexEqualToStartIndexAndCoversCustomLine) { + let maximumSpecialHeightOnDeletedInterval = 0; + for (let i = startIndexOfDeletion; i <= endIndexOfDeletion; i++) { + maximumSpecialHeightOnDeletedInterval = Math.max(maximumSpecialHeightOnDeletedInterval, this._orderedCustomLines[i].maximumSpecialHeight); + } + let prefixSumOnDeletedInterval = 0; + if (startIndexOfDeletion > 0) { + const previousSpecialLine = this._orderedCustomLines[startIndexOfDeletion - 1]; + prefixSumOnDeletedInterval = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (fromLineNumber - previousSpecialLine.lineNumber - 1); + } else { + prefixSumOnDeletedInterval = fromLineNumber > 0 ? (fromLineNumber - 1) * this._defaultLineHeight : 0; + } + const firstSpecialLineDeleted = this._orderedCustomLines[startIndexOfDeletion]; + const lastSpecialLineDeleted = this._orderedCustomLines[endIndexOfDeletion]; + const firstSpecialLineAfterDeletion = this._orderedCustomLines[endIndexOfDeletion + 1]; + const heightOfFirstLineAfterDeletion = firstSpecialLineAfterDeletion && firstSpecialLineAfterDeletion.lineNumber === toLineNumber + 1 ? firstSpecialLineAfterDeletion.maximumSpecialHeight : this._defaultLineHeight; + const totalHeightDeleted = lastSpecialLineDeleted.prefixSum + + lastSpecialLineDeleted.maximumSpecialHeight + - firstSpecialLineDeleted.prefixSum + + this._defaultLineHeight * (toLineNumber - lastSpecialLineDeleted.lineNumber) + + this._defaultLineHeight * (firstSpecialLineDeleted.lineNumber - fromLineNumber) + + heightOfFirstLineAfterDeletion - maximumSpecialHeightOnDeletedInterval; + + const decorationIdsSeen = new Set(); + const newOrderedCustomLines: CustomLine[] = []; + const newDecorationIDToSpecialLine = new ArrayMap(); + let numberOfDeletions = 0; + for (let i = 0; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + if (i < startIndexOfDeletion) { + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } else if (i >= startIndexOfDeletion && i <= endIndexOfDeletion) { + const decorationId = customLine.decorationId; + if (!decorationIdsSeen.has(decorationId)) { + customLine.index -= numberOfDeletions; + customLine.lineNumber = fromLineNumber; + customLine.prefixSum = prefixSumOnDeletedInterval; + customLine.maximumSpecialHeight = maximumSpecialHeightOnDeletedInterval; + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } else { + numberOfDeletions++; + } + } else if (i > endIndexOfDeletion) { + customLine.index -= numberOfDeletions; + customLine.lineNumber -= deleteCount; + customLine.prefixSum -= totalHeightDeleted; + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + decorationIdsSeen.add(customLine.decorationId); + } + this._orderedCustomLines = newOrderedCustomLines; + this._decorationIDToCustomLine = newDecorationIDToSpecialLine; + } else { + const totalHeightDeleted = deleteCount * this._defaultLineHeight; + for (let i = endIndexOfDeletion; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + customLine.lineNumber -= deleteCount; + customLine.prefixSum -= totalHeightDeleted; + } + } + } + + public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + const insertCount = toLineNumber - fromLineNumber + 1; + const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); + let startIndexOfInsertion: number; + if (candidateStartIndexOfInsertion >= 0) { + startIndexOfInsertion = candidateStartIndexOfInsertion; + for (let i = candidateStartIndexOfInsertion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + startIndexOfInsertion--; + } else { + break; + } + } + } else { + startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1); + } + const toReAdd: ICustomLineHeightData[] = []; + const decorationsImmediatelyAfter = new Set(); + for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + decorationsImmediatelyAfter.add(this._orderedCustomLines[i].decorationId); + } + } + const decorationsImmediatelyBefore = new Set(); + for (let i = startIndexOfInsertion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber - 1) { + decorationsImmediatelyBefore.add(this._orderedCustomLines[i].decorationId); + } + } + const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter); + for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { + this._orderedCustomLines[i].lineNumber += insertCount; + this._orderedCustomLines[i].prefixSum += this._defaultLineHeight * insertCount; + } + + if (decorationsWithGaps.size > 0) { + for (const decorationId of decorationsWithGaps) { + const decoration = this._decorationIDToCustomLine.get(decorationId); + if (decoration) { + const startLineNumber = decoration.reduce((min, l) => Math.min(min, l.lineNumber), fromLineNumber); // min + const endLineNumber = decoration.reduce((max, l) => Math.max(max, l.lineNumber), fromLineNumber); // max + const lineHeight = decoration.reduce((max, l) => Math.max(max, l.specialHeight), 0); + toReAdd.push({ + decorationId, + startLineNumber, + endLineNumber, + lineHeight + }); + } + } + + for (const dec of toReAdd) { + this.insertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight); + } + this.commit(); + } + } + + public commit(): void { + if (!this._hasPending) { + return; + } + for (const pendingChange of this._pendingSpecialLinesToInsert) { + const candidateInsertionIndex = this._binarySearchOverOrderedCustomLinesArray(pendingChange.lineNumber); + const insertionIndex = candidateInsertionIndex >= 0 ? candidateInsertionIndex : -(candidateInsertionIndex + 1); + this._orderedCustomLines.splice(insertionIndex, 0, pendingChange); + this._invalidIndex = Math.min(this._invalidIndex, insertionIndex); + } + this._pendingSpecialLinesToInsert = []; + const newDecorationIDToSpecialLine = new ArrayMap(); + const newOrderedSpecialLines: CustomLine[] = []; + + for (let i = 0; i < this._invalidIndex; i++) { + const customLine = this._orderedCustomLines[i]; + newOrderedSpecialLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + + let numberOfDeletions = 0; + let previousSpecialLine: CustomLine | undefined = (this._invalidIndex > 0) ? newOrderedSpecialLines[this._invalidIndex - 1] : undefined; + for (let i = this._invalidIndex; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + if (customLine.deleted) { + numberOfDeletions++; + continue; + } + customLine.index = i - numberOfDeletions; + if (previousSpecialLine && previousSpecialLine.lineNumber === customLine.lineNumber) { + customLine.maximumSpecialHeight = previousSpecialLine.maximumSpecialHeight; + customLine.prefixSum = previousSpecialLine.prefixSum; + } else { + let maximumSpecialHeight = customLine.specialHeight; + for (let j = i; j < this._orderedCustomLines.length; j++) { + const nextSpecialLine = this._orderedCustomLines[j]; + if (nextSpecialLine.deleted) { + continue; + } + if (nextSpecialLine.lineNumber !== customLine.lineNumber) { + break; + } + maximumSpecialHeight = Math.max(maximumSpecialHeight, nextSpecialLine.specialHeight); + } + customLine.maximumSpecialHeight = maximumSpecialHeight; + + let prefixSum: number; + if (previousSpecialLine) { + prefixSum = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (customLine.lineNumber - previousSpecialLine.lineNumber - 1); + } else { + prefixSum = this._defaultLineHeight * (customLine.lineNumber - 1); + } + customLine.prefixSum = prefixSum; + } + previousSpecialLine = customLine; + newOrderedSpecialLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + this._orderedCustomLines = newOrderedSpecialLines; + this._decorationIDToCustomLine = newDecorationIDToSpecialLine; + this._invalidIndex = Infinity; + this._hasPending = false; + } + + private _binarySearchOverOrderedCustomLinesArray(lineNumber: number): number { + return binarySearch2(this._orderedCustomLines.length, (index) => { + const line = this._orderedCustomLines[index]; + if (line.lineNumber === lineNumber) { + return 0; + } else if (line.lineNumber < lineNumber) { + return -1; + } else { + return 1; + } + }); + } +} + +export interface ICustomLineHeightData { + readonly decorationId: string; + readonly startLineNumber: number; + readonly endLineNumber: number; + readonly lineHeight: number; +} + +class ArrayMap { + + private _map: Map = new Map(); + + constructor() { } + + add(key: K, value: T) { + const array = this._map.get(key); + if (!array) { + this._map.set(key, [value]); + } else { + array.push(value); + } + } + + get(key: K): T[] | undefined { + return this._map.get(key); + } + + delete(key: K): void { + this._map.delete(key); + } +} diff --git a/code/src/vs/editor/common/viewLayout/linesLayout.ts b/code/src/vs/editor/common/viewLayout/linesLayout.ts index f7988abddce..b773e5dd4cb 100644 --- a/code/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/code/src/vs/editor/common/viewLayout/linesLayout.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorWhitespace, IPartialViewLinesViewportData, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; +import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; import * as strings from '../../../base/common/strings.js'; +import { ICustomLineHeightData, LineHeightsManager } from './lineHeights.js'; interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number } interface IPendingRemove { id: string } @@ -37,10 +38,6 @@ class PendingChanges { this._removes.push(x); } - public mustCommit(): boolean { - return this._hasPending; - } - public commit(linesLayout: LinesLayout): void { if (!this._hasPending) { return; @@ -94,11 +91,11 @@ export class LinesLayout { private _prefixSumValidIndex: number; private _minWidth: number; private _lineCount: number; - private _lineHeight: number; private _paddingTop: number; private _paddingBottom: number; + private _lineHeightsManager: LineHeightsManager; - constructor(lineCount: number, lineHeight: number, paddingTop: number, paddingBottom: number) { + constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: ICustomLineHeightData[]) { this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT); this._pendingChanges = new PendingChanges(); this._lastWhitespaceId = 0; @@ -106,9 +103,9 @@ export class LinesLayout { this._prefixSumValidIndex = -1; this._minWidth = -1; /* marker for not being computed */ this._lineCount = lineCount; - this._lineHeight = lineHeight; this._paddingTop = paddingTop; this._paddingBottom = paddingBottom; + this._lineHeightsManager = new LineHeightsManager(defaultLineHeight, customLineHeightData); } /** @@ -141,9 +138,8 @@ export class LinesLayout { /** * Change the height of a line in pixels. */ - public setLineHeight(lineHeight: number): void { - this._checkPendingChanges(); - this._lineHeight = lineHeight; + public setDefaultLineHeight(lineHeight: number): void { + this._lineHeightsManager.defaultLineHeight = lineHeight; } /** @@ -159,9 +155,29 @@ export class LinesLayout { * * @param lineCount New number of lines. */ - public onFlushed(lineCount: number): void { - this._checkPendingChanges(); + public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { this._lineCount = lineCount; + this._lineHeightsManager = new LineHeightsManager(this._lineHeightsManager.defaultLineHeight, customLineHeightData); + } + + public changeLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean { + let hadAChange = false; + try { + const accessor: ILineHeightChangeAccessor = { + insertOrChangeCustomLineHeight: (decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void => { + hadAChange = true; + this._lineHeightsManager.insertOrChangeCustomLineHeight(decorationId, startLineNumber, endLineNumber, lineHeight); + }, + removeCustomLineHeight: (decorationId: string): void => { + hadAChange = true; + this._lineHeightsManager.removeCustomLineHeight(decorationId); + } + }; + callback(accessor); + } finally { + this._lineHeightsManager.commit(); + } + return hadAChange; } public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean { @@ -259,12 +275,6 @@ export class LinesLayout { this._prefixSumValidIndex = -1; } - private _checkPendingChanges(): void { - if (this._pendingChanges.mustCommit()) { - this._pendingChanges.commit(this); - } - } - private _insertWhitespace(whitespace: EditorWhitespace): void { const insertIndex = LinesLayout.findInsertionIndex(this._arr, whitespace.afterLineNumber, whitespace.ordinal); this._arr.splice(insertIndex, 0, whitespace); @@ -318,7 +328,6 @@ export class LinesLayout { * @param toLineNumber The line number at which the deletion ended, inclusive */ public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { - this._checkPendingChanges(); fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -336,6 +345,7 @@ export class LinesLayout { this._arr[i].afterLineNumber -= (toLineNumber - fromLineNumber + 1); } } + this._lineHeightsManager.onLinesDeleted(fromLineNumber, toLineNumber); } /** @@ -345,7 +355,6 @@ export class LinesLayout { * @param toLineNumber The line number at which the insertion ended, inclusive. */ public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { - this._checkPendingChanges(); fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -357,13 +366,13 @@ export class LinesLayout { this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); } } + this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber); } /** * Get the sum of all the whitespaces. */ public getWhitespacesTotalHeight(): number { - this._checkPendingChanges(); if (this._arr.length === 0) { return 0; } @@ -378,7 +387,6 @@ export class LinesLayout { * @return The sum of the heights of all whitespaces before the one at `index`, including the one at `index`. */ public getWhitespacesAccumulatedHeight(index: number): number { - this._checkPendingChanges(); index = index | 0; let startIndex = Math.max(0, this._prefixSumValidIndex + 1); @@ -400,8 +408,7 @@ export class LinesLayout { * @return The sum of heights for all objects. */ public getLinesTotalHeight(): number { - this._checkPendingChanges(); - const linesHeight = this._lineHeight * this._lineCount; + const linesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(this._lineCount); const whitespacesHeight = this.getWhitespacesTotalHeight(); return linesHeight + whitespacesHeight + this._paddingTop + this._paddingBottom; @@ -413,7 +420,6 @@ export class LinesLayout { * @param lineNumber The line number */ public getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber: number): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); @@ -470,7 +476,6 @@ export class LinesLayout { * @return The index of the first whitespace with `afterLineNumber` >= `lineNumber` or -1 if no whitespace is found. */ public getFirstWhitespaceIndexAfterLineNumber(lineNumber: number): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; return this._findFirstWhitespaceAfterLineNumber(lineNumber); @@ -483,12 +488,11 @@ export class LinesLayout { * @return The sum of heights for all objects above `lineNumber`. */ public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones = false): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; let previousLinesHeight: number; if (lineNumber > 1) { - previousLinesHeight = this._lineHeight * (lineNumber - 1); + previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber - 1); } else { previousLinesHeight = 0; } @@ -498,16 +502,19 @@ export class LinesLayout { return previousLinesHeight + previousWhitespacesHeight + this._paddingTop; } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._lineHeightsManager.heightForLineNumber(lineNumber); + } + /** - * Get the vertical offset (the sum of heights for all objects above) a certain line number. + * Get the vertical offset (the sum of heights for all objects above) a certain line number and also the line height of the line. * * @param lineNumber The line number * @return The sum of heights for all objects above `lineNumber`. */ public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones = false): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; - const previousLinesHeight = this._lineHeight * lineNumber; + const previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber); const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber + (includeViewZones ? 1 : 0)); return previousLinesHeight + previousWhitespacesHeight + this._paddingTop; } @@ -516,7 +523,6 @@ export class LinesLayout { * Returns if there is any whitespace in the document. */ public hasWhitespace(): boolean { - this._checkPendingChanges(); return this.getWhitespacesCount() > 0; } @@ -524,7 +530,6 @@ export class LinesLayout { * The maximum min width for all whitespaces. */ public getWhitespaceMinWidth(): number { - this._checkPendingChanges(); if (this._minWidth === -1) { let minWidth = 0; for (let i = 0, len = this._arr.length; i < len; i++) { @@ -539,7 +544,6 @@ export class LinesLayout { * Check if `verticalOffset` is below all lines. */ public isAfterLines(verticalOffset: number): boolean { - this._checkPendingChanges(); const totalHeight = this.getLinesTotalHeight(); return verticalOffset > totalHeight; } @@ -548,7 +552,6 @@ export class LinesLayout { if (this._paddingTop === 0) { return false; } - this._checkPendingChanges(); return (verticalOffset < this._paddingTop); } @@ -556,7 +559,6 @@ export class LinesLayout { if (this._paddingBottom === 0) { return false; } - this._checkPendingChanges(); const totalHeight = this.getLinesTotalHeight(); return (verticalOffset >= totalHeight - this._paddingBottom); } @@ -570,7 +572,6 @@ export class LinesLayout { * @return The line number at or after vertical offset `verticalOffset`. */ public getLineNumberAtOrAfterVerticalOffset(verticalOffset: number): number { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; if (verticalOffset < 0) { @@ -578,13 +579,13 @@ export class LinesLayout { } const linesCount = this._lineCount | 0; - const lineHeight = this._lineHeight; let minLineNumber = 1; let maxLineNumber = linesCount; while (minLineNumber < maxLineNumber) { const midLineNumber = ((minLineNumber + maxLineNumber) / 2) | 0; + const lineHeight = this.getLineHeightForLineNumber(midLineNumber); const midLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(midLineNumber) | 0; if (verticalOffset >= midLineNumberVerticalOffset + lineHeight) { @@ -614,10 +615,8 @@ export class LinesLayout { * @return A structure describing the lines positioned between `verticalOffset1` and `verticalOffset2`. */ public getLinesViewportData(verticalOffset1: number, verticalOffset2: number): IPartialViewLinesViewportData { - this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; - const lineHeight = this._lineHeight; // Find first line number // We don't live in a perfect world, so the line number might start before or after verticalOffset1 @@ -650,7 +649,7 @@ export class LinesLayout { if (startLineNumberVerticalOffset >= STEP_SIZE) { // Compute a delta that guarantees that lines are positioned at `lineHeight` increments bigNumbersDelta = Math.floor(startLineNumberVerticalOffset / STEP_SIZE) * STEP_SIZE; - bigNumbersDelta = Math.floor(bigNumbersDelta / lineHeight) * lineHeight; + bigNumbersDelta = Math.floor(bigNumbersDelta / this._lineHeightsManager.defaultLineHeight) * this._lineHeightsManager.defaultLineHeight; currentLineRelativeOffset -= bigNumbersDelta; } @@ -662,7 +661,7 @@ export class LinesLayout { // Figure out how far the lines go for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - + const lineHeight = this.getLineHeightForLineNumber(lineNumber); if (centeredLineNumber === -1) { const currentLineTop = currentVerticalOffset; const currentLineBottom = currentVerticalOffset + lineHeight; @@ -715,7 +714,8 @@ export class LinesLayout { } } if (completelyVisibleStartLineNumber < completelyVisibleEndLineNumber) { - if (endLineNumberVerticalOffset + lineHeight > verticalOffset2) { + const endLineHeight = this.getLineHeightForLineNumber(endLineNumber); + if (endLineNumberVerticalOffset + endLineHeight > verticalOffset2) { completelyVisibleEndLineNumber--; } } @@ -728,19 +728,18 @@ export class LinesLayout { centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, - lineHeight: this._lineHeight, + lineHeight: this._lineHeightsManager.defaultLineHeight, }; } public getVerticalOffsetForWhitespaceIndex(whitespaceIndex: number): number { - this._checkPendingChanges(); whitespaceIndex = whitespaceIndex | 0; const afterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex); let previousLinesHeight: number; if (afterLineNumber >= 1) { - previousLinesHeight = this._lineHeight * afterLineNumber; + previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(afterLineNumber); } else { previousLinesHeight = 0; } @@ -755,7 +754,6 @@ export class LinesLayout { } public getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset: number): number { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; let minWhitespaceIndex = 0; @@ -799,7 +797,6 @@ export class LinesLayout { * @return Precisely the whitespace that is layouted at `verticaloffset` or null. */ public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; const candidateIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset); @@ -838,7 +835,6 @@ export class LinesLayout { * @return An array with all the whitespaces in the viewport. If no whitespace is in viewport, the array is empty. */ public getWhitespaceViewportData(verticalOffset1: number, verticalOffset2: number): IViewWhitespaceViewportData[] { - this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; @@ -872,7 +868,6 @@ export class LinesLayout { * Get all whitespaces. */ public getWhitespaces(): IEditorWhitespace[] { - this._checkPendingChanges(); return this._arr.slice(0); } @@ -880,7 +875,6 @@ export class LinesLayout { * The number of whitespaces. */ public getWhitespacesCount(): number { - this._checkPendingChanges(); return this._arr.length; } @@ -891,7 +885,6 @@ export class LinesLayout { * @return `id` of whitespace at `index`. */ public getIdForWhitespaceIndex(index: number): string { - this._checkPendingChanges(); index = index | 0; return this._arr[index].id; @@ -904,7 +897,6 @@ export class LinesLayout { * @return `afterLineNumber` of whitespace at `index`. */ public getAfterLineNumberForWhitespaceIndex(index: number): number { - this._checkPendingChanges(); index = index | 0; return this._arr[index].afterLineNumber; @@ -917,7 +909,6 @@ export class LinesLayout { * @return `height` of whitespace at `index`. */ public getHeightForWhitespaceIndex(index: number): number { - this._checkPendingChanges(); index = index | 0; return this._arr[index].height; diff --git a/code/src/vs/editor/common/viewLayout/viewLayout.ts b/code/src/vs/editor/common/viewLayout/viewLayout.ts index 767a308db9a..404a5823b04 100644 --- a/code/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/code/src/vs/editor/common/viewLayout/viewLayout.ts @@ -10,8 +10,9 @@ import { ConfigurationChangedEvent, EditorOption } from '../config/editorOptions import { ScrollType } from '../editorCommon.js'; import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { LinesLayout } from './linesLayout.js'; -import { IEditorWhitespace, IPartialViewLinesViewportData, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; +import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js'; +import { ICustomLineHeightData } from './lineHeights.js'; const SMOOTH_SCROLLING_TIME = 125; @@ -163,7 +164,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public readonly onDidScroll: Event; public readonly onDidContentSizeChange: Event; - constructor(configuration: IEditorConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this._configuration = configuration; @@ -171,7 +172,7 @@ export class ViewLayout extends Disposable implements IViewLayout { const layoutInfo = options.get(EditorOption.layoutInfo); const padding = options.get(EditorOption.padding); - this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom); + this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom, customLineHeightData); this._maxLineWidth = 0; this._overlayWidgetsMinWidth = 0; @@ -211,7 +212,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public onConfigurationChanged(e: ConfigurationChangedEvent): void { const options = this._configuration.options; if (e.hasChanged(EditorOption.lineHeight)) { - this._linesLayout.setLineHeight(options.get(EditorOption.lineHeight)); + this._linesLayout.setDefaultLineHeight(options.get(EditorOption.lineHeight)); } if (e.hasChanged(EditorOption.padding)) { const padding = options.get(EditorOption.padding); @@ -236,8 +237,8 @@ export class ViewLayout extends Disposable implements IViewLayout { this._configureSmoothScrollDuration(); } } - public onFlushed(lineCount: number): void { - this._linesLayout.onFlushed(lineCount); + public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + this._linesLayout.onFlushed(lineCount, customLineHeightData); } public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber); @@ -380,12 +381,24 @@ export class ViewLayout extends Disposable implements IViewLayout { } return hadAChange; } + + public changeSpecialLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean { + const hadAChange = this._linesLayout.changeLineHeights(callback); + if (hadAChange) { + this.onHeightMaybeChanged(); + } + return hadAChange; + } + public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones: boolean = false): number { return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones); } public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones: boolean = false): number { return this._linesLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._linesLayout.getLineHeightForLineNumber(lineNumber); + } public isAfterLines(verticalOffset: number): boolean { return this._linesLayout.isAfterLines(verticalOffset); } diff --git a/code/src/vs/editor/common/viewModel.ts b/code/src/vs/editor/common/viewModel.ts index 1aefa04abeb..d9233c5c06c 100644 --- a/code/src/vs/editor/common/viewModel.ts +++ b/code/src/vs/editor/common/viewModel.ts @@ -133,6 +133,7 @@ export interface IViewLayout { getLineNumberAtVerticalOffset(verticalOffset: number): number; getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones?: boolean): number; getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones?: boolean): number; + getLineHeightForLineNumber(lineNumber: number): number; getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null; /** @@ -156,6 +157,11 @@ export interface IWhitespaceChangeAccessor { removeWhitespace(id: string): void; } +export interface ILineHeightChangeAccessor { + insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void; + removeCustomLineHeight(decorationId: string): void; +} + export interface IPartialViewLinesViewportData { /** * Value to be substracted from `scrollTop` (in order to vertical offset numbers < 1MM) diff --git a/code/src/vs/editor/common/viewModel/viewModelImpl.ts b/code/src/vs/editor/common/viewModel/viewModelImpl.ts index 7fe34c92995..09d842ecd35 100644 --- a/code/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/code/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -34,12 +34,13 @@ import { ViewLayout } from '../viewLayout/viewLayout.js'; import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js'; import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; import { ViewEventHandler } from '../viewEventHandler.js'; -import { ICoordinatesConverter, InlineDecoration, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; +import { ICoordinatesConverter, InlineDecoration, ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; -import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; +import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelLineHeightChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; +import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -116,7 +117,7 @@ export class ViewModel extends Disposable implements IViewModel { this._cursor = this._register(new CursorsController(model, this, this.coordinatesConverter, this.cursorConfig)); - this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); + this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), this._getCustomLineHeights(), scheduleAtNextAnimationFrame)); this._register(this.viewLayout.onDidScroll((e) => { if (e.scrollTopChanged) { @@ -183,6 +184,20 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.removeViewEventHandler(eventHandler); } + private _getCustomLineHeights(): ICustomLineHeightData[] { + const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); + return decorations.map((d) => { + const lineNumber = d.range.startLineNumber; + const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); + return { + decorationId: d.id, + startLineNumber: viewRange.startLineNumber, + endLineNumber: viewRange.endLineNumber, + lineHeight: d.options.lineHeight || 0 + }; + }); + } + private _updateConfigurationViewLineCountNow(): void { this._configuration.setViewLineCount(this._lines.getViewLineCount()); } @@ -254,7 +269,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); this._updateConfigurationViewLineCount.schedule(); } @@ -327,7 +342,7 @@ export class ViewModel extends Disposable implements IViewModel { this._lines.onModelFlushed(); eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); this._decorations.reset(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); hadOtherModelChange = true; break; } @@ -419,6 +434,28 @@ export class ViewModel extends Disposable implements IViewModel { this._handleVisibleLinesChanged(); })); + this._register(this.model.onDidChangeLineHeight((e) => { + const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0); + + this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { + for (const change of filteredChanges) { + const { decorationId, lineNumber, lineHeight } = change; + const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); + if (lineHeight !== null) { + accessor.insertOrChangeCustomLineHeight(decorationId, viewRange.startLineNumber, viewRange.endLineNumber, lineHeight); + } else { + accessor.removeCustomLineHeight(decorationId); + } + } + }); + + // recreate the model event using the filtered changes + if (filteredChanges.length > 0) { + const filteredEvent = new textModelEvents.ModelLineHeightChangedEvent(filteredChanges); + this._eventDispatcher.emitOutgoingEvent(new ModelLineHeightChangedEvent(filteredEvent)); + } + })); + this._register(this.model.onDidChangeTokens((e) => { const viewRanges: { fromLineNumber: number; toLineNumber: number }[] = []; for (let j = 0, lenJ = e.ranges.length; j < lenJ; j++) { @@ -457,7 +494,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); } finally { this._eventDispatcher.endEmitViewEvents(); } @@ -506,7 +543,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); this.viewLayout.onHeightMaybeChanged(); } diff --git a/code/src/vs/editor/common/viewModelEventDispatcher.ts b/code/src/vs/editor/common/viewModelEventDispatcher.ts index fc91875b319..81516adf725 100644 --- a/code/src/vs/editor/common/viewModelEventDispatcher.ts +++ b/code/src/vs/editor/common/viewModelEventDispatcher.ts @@ -10,7 +10,7 @@ import { Emitter } from '../../base/common/event.js'; import { Selection } from './core/selection.js'; import { Disposable } from '../../base/common/lifecycle.js'; import { CursorChangeReason } from './cursorEvents.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from './textModelEvents.js'; +import { ModelLineHeightChangedEvent as OriginalModelLineHeightChangedEvent, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from './textModelEvents.js'; export class ViewModelEventDispatcher extends Disposable { @@ -188,6 +188,7 @@ export type OutgoingViewModelEvent = ( | ModelContentChangedEvent | ModelOptionsChangedEvent | ModelTokensChangedEvent + | ModelLineHeightChangedEvent ); export const enum OutgoingViewModelEventKind { @@ -205,6 +206,7 @@ export const enum OutgoingViewModelEventKind { ModelContentChanged, ModelOptionsChanged, ModelTokensChanged, + ModelLineHeightChanged, } export class ContentSizeChangedEvent implements IContentSizeChangedEvent { @@ -553,3 +555,19 @@ export class ModelTokensChangedEvent { return null; } } + +export class ModelLineHeightChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelLineHeightChanged; + + constructor( + public readonly event: OriginalModelLineHeightChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} diff --git a/code/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/code/src/vs/editor/contrib/clipboard/browser/clipboard.ts index d3fe79f1424..6a382131fce 100644 --- a/code/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/code/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -4,15 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as browser from '../../../../base/browser/browser.js'; -import { getActiveDocument } from '../../../../base/browser/dom.js'; +import { getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import * as platform from '../../../../base/common/platform.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; import * as nls from '../../../../nls.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CopyOptions, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; import { NativeEditContextRegistry } from '../../../browser/controller/editContext/native/nativeEditContextRegistry.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; @@ -229,26 +232,46 @@ if (PasteAction) { PasteAction.addImplementation(10000, 'code-editor', (accessor: ServicesAccessor, args: any) => { const codeEditorService = accessor.get(ICodeEditorService); const clipboardService = accessor.get(IClipboardService); + const telemetryService = accessor.get(ITelemetryService); + const productService = accessor.get(IProductService); // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = codeEditorService.getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasModel() && focusedEditor.hasTextFocus()) { // execCommand(paste) does not work with edit context - let result: boolean; const experimentalEditContextEnabled = focusedEditor.getOption(EditorOption.effectiveExperimentalEditContextEnabled); if (experimentalEditContextEnabled) { const nativeEditContext = NativeEditContextRegistry.get(focusedEditor.getId()); if (nativeEditContext) { - result = nativeEditContext.executePaste(); - } else { - result = false; + nativeEditContext.onWillPaste(); } - } else { - result = focusedEditor.getContainerDomNode().ownerDocument.execCommand('paste'); } - if (result) { - return CopyPasteController.get(focusedEditor)?.finishedPaste() ?? Promise.resolve(); - } else if (platform.isWeb) { + + const sw = StopWatch.create(true); + const triggerPaste = clipboardService.triggerPaste(getActiveWindow().vscodeWindowId); + if (triggerPaste) { + return triggerPaste.then(async () => { + + if (productService.quality !== 'stable') { + const duration = sw.elapsed(); + type EditorAsyncPasteClassification = { + duration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration of the paste operation.' }; + owner: 'aiday-mar'; + comment: 'Provides insight into the delay introduced by pasting async via keybindings.'; + }; + type EditorAsyncPasteEvent = { + duration: number; + }; + telemetryService.publicLog2( + 'editorAsyncPaste', + { duration } + ); + } + + return CopyPasteController.get(focusedEditor)?.finishedPaste() ?? Promise.resolve(); + }); + } + if (platform.isWeb) { // Use the clipboard service if document.execCommand('paste') was not successful return (async () => { const clipboardText = await clipboardService.readText(); @@ -278,8 +301,8 @@ if (PasteAction) { // 2. Paste: (default) handle case when focus is somewhere else. PasteAction.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: any) => { - getActiveDocument().execCommand('paste'); - return true; + const triggerPaste = accessor.get(IClipboardService).triggerPaste(getActiveWindow().vscodeWindowId); + return triggerPaste ?? false; }); } diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts index c78ecc080d4..09a9c11a690 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -164,7 +164,9 @@ export async function getCodeActions( ...coalesce(actions.map(x => x.documentation)), ...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions) ]; - return new ManagedCodeActionSet(allActions, allDocumentation, disposables); + const managedCodeActionSet = new ManagedCodeActionSet(allActions, allDocumentation, disposables); + disposables.add(managedCodeActionSet); + return managedCodeActionSet; } catch (err) { disposables.dispose(); throw err; diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index d5e249add65..63642ee0a04 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -232,6 +232,7 @@ export class CodeActionModel extends Disposable { const actions = createCancelablePromise(async token => { if (this._settingEnabledNearbyQuickfixes() && trigger.trigger.type === CodeActionTriggerType.Invoke && (trigger.trigger.triggerAction === CodeActionTriggerSource.QuickFix || trigger.trigger.filter?.include?.contains(CodeActionKind.QuickFix))) { const codeActionSet = await getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); + this.codeActionsDisposable.value = codeActionSet; const allCodeActions = [...codeActionSet.allActions]; if (token.isCancellationRequested) { codeActionSet.dispose(); @@ -326,6 +327,7 @@ export class CodeActionModel extends Disposable { // Case for manual triggers - specifically Source Actions and Refactors if (trigger.trigger.type === CodeActionTriggerType.Invoke) { const codeActions = await getCodeActions(this._registry, model, trigger.selection, trigger.trigger, Progress.None, token); + this.codeActionsDisposable.value = codeActions; return codeActions; } @@ -365,7 +367,7 @@ export class CodeActionModel extends Disposable { public trigger(trigger: CodeActionTrigger) { this._codeActionOracle.value?.trigger(trigger); - this.codeActionsDisposable.clear(); + this.codeActionsDisposable.dispose(); } private setState(newState: CodeActionsState.State, skipNotify?: boolean) { diff --git a/code/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/code/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index e6043ebbd33..b5a12f416cd 100644 --- a/code/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/code/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -47,7 +47,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { private readonly _ruleFactory: DynamicCssRules; - private readonly _decoratorLimitReporter = new DecoratorLimitReporter(); + private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter()); constructor( private readonly _editor: ICodeEditor, @@ -269,8 +269,8 @@ export class ColorDetector extends Disposable implements IEditorContribution { } } -export class DecoratorLimitReporter { - private _onDidChange = new Emitter(); +export class DecoratorLimitReporter extends Disposable { + private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; private _computed: number = 0; diff --git a/code/src/vs/editor/contrib/find/browser/findController.ts b/code/src/vs/editor/contrib/find/browser/findController.ts index 9e00a1d5d47..5ce76b9ccc8 100644 --- a/code/src/vs/editor/contrib/find/browser/findController.ts +++ b/code/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/code/src/vs/editor/contrib/find/browser/findWidget.ts b/code/src/vs/editor/contrib/find/browser/findWidget.ts index e60addcd084..24be44fddb5 100644 --- a/code/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/code/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/code/src/vs/editor/contrib/folding/browser/folding.ts b/code/src/vs/editor/contrib/folding/browser/folding.ts index dedd1ce7964..edbc626e7b0 100644 --- a/code/src/vs/editor/contrib/folding/browser/folding.ts +++ b/code/src/vs/editor/contrib/folding/browser/folding.ts @@ -124,7 +124,7 @@ export class FoldingController extends Disposable implements IEditorContribution super(); this.editor = editor; - this._foldingLimitReporter = new RangesLimitReporter(editor); + this._foldingLimitReporter = this._register(new RangesLimitReporter(editor)); const options = this.editor.getOptions(); this._isEnabled = options.get(EditorOption.folding); @@ -513,15 +513,16 @@ export class FoldingController extends Disposable implements IEditorContribution } } -export class RangesLimitReporter implements FoldingLimitReporter { +export class RangesLimitReporter extends Disposable implements FoldingLimitReporter { constructor(private readonly editor: ICodeEditor) { + super(); } public get limit() { return this.editor.getOptions().get(EditorOption.foldingMaximumRegions); } - private _onDidChange = new Emitter(); + private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; private _computed: number = 0; diff --git a/code/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts b/code/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts index e02392bdad9..c1cd8f14db4 100644 --- a/code/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts +++ b/code/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts @@ -19,7 +19,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, IStatusHandle } from '../../../../platform/notification/common/notification.js'; export const ctxHasSymbols = new RawContextKey('hasSymbols', false, localize('hasSymbols', "Whether there are symbol locations that can be navigated via keyboard-only.")); @@ -41,7 +41,7 @@ class SymbolNavigationService implements ISymbolNavigationService { private _currentModel?: ReferencesModel = undefined; private _currentIdx: number = -1; private _currentState?: IDisposable; - private _currentMessage?: IDisposable; + private _currentMessage?: IStatusHandle; private _ignoreEditorChange: boolean = false; constructor( @@ -56,7 +56,7 @@ class SymbolNavigationService implements ISymbolNavigationService { reset(): void { this._ctxHasSymbols.reset(); this._currentState?.dispose(); - this._currentMessage?.dispose(); + this._currentMessage?.close(); this._currentModel = undefined; this._currentIdx = -1; } @@ -138,7 +138,7 @@ class SymbolNavigationService implements ISymbolNavigationService { private _showMessage(): void { - this._currentMessage?.dispose(); + this._currentMessage?.close(); const kb = this._keybindingService.lookupKeybinding('editor.gotoNextSymbolFromResult'); const message = kb diff --git a/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 03b585a99b1..39b2d68e438 100644 --- a/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/code/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -312,7 +312,7 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { markdownHover: MarkdownHover, onFinishedRendering: () => void ): IRenderedHoverPart { - const renderedMarkdownHover = renderMarkdownInContainer( + const renderedMarkdownHover = renderMarkdown( this._editor, markdownHover, this._languageService, @@ -484,18 +484,20 @@ export function renderMarkdownHovers( markdownHovers.sort(compareBy(hover => hover.ordinal, numberComparator)); const renderedHoverParts: IRenderedHoverPart[] = []; for (const markdownHover of markdownHovers) { - renderedHoverParts.push(renderMarkdownInContainer( + const renderedHoverPart = renderMarkdown( editor, markdownHover, languageService, openerService, context.onContentsChanged, - )); + ); + context.fragment.appendChild(renderedHoverPart.hoverElement); + renderedHoverParts.push(renderedHoverPart); } return new RenderedHoverParts(renderedHoverParts); } -function renderMarkdownInContainer( +function renderMarkdown( editor: ICodeEditor, markdownHover: MarkdownHover, languageService: ILanguageService, diff --git a/code/src/vs/editor/contrib/indentation/browser/indentation.ts b/code/src/vs/editor/contrib/indentation/browser/indentation.ts index e467efa9ac0..41e9b7fbd32 100644 --- a/code/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/code/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -386,7 +386,7 @@ export class AutoIndentOnPaste implements IEditorContribution { this.callOnModel.clear(); // we are disabled - if (this.editor.getOption(EditorOption.autoIndent) < EditorAutoIndentStrategy.Full || this.editor.getOption(EditorOption.formatOnPaste)) { + if (this.editor.getOption(EditorOption.autoIndent) < EditorAutoIndentStrategy.Full || !this.editor.getOption(EditorOption.formatOnPaste)) { return; } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index cb1b72e2125..4f203a95a52 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -7,6 +7,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { asyncTransaction, transaction } from '../../../../../base/common/observable.js'; import { splitLines } from '../../../../../base/common/strings.js'; import * as nls from '../../../../../nls.js'; +import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -126,7 +127,7 @@ export class AcceptNextWordOfInlineCompletion extends EditorAction { kbOpts: { weight: KeybindingWeight.EditorContrib + 1, primary: KeyMod.CtrlCmd | KeyCode.RightArrow, - kbExpr: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineSuggestionVisible), + kbExpr: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineSuggestionVisible, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), }, menuOpts: [{ menuId: MenuId.InlineSuggestionToolbar, @@ -139,7 +140,7 @@ export class AcceptNextWordOfInlineCompletion extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineCompletionsController.get(editor); - await controller?.model.get()?.acceptNextWord(controller.editor); + await controller?.model.get()?.acceptNextWord(); } } @@ -163,7 +164,7 @@ export class AcceptNextLineOfInlineCompletion extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineCompletionsController.get(editor); - await controller?.model.get()?.acceptNextLine(controller.editor); + await controller?.model.get()?.acceptNextLine(); } } @@ -261,25 +262,6 @@ export class JumpToNextInlineEdit extends EditorAction { } } -export class AcceptNextInlineEditPart extends EditorAction { - constructor() { - super({ - id: 'editor.action.inlineSuggest.acceptNextInlineEditPart', - label: nls.localize2('action.inlineSuggest.acceptNextInlineEditPart', "Accept Next Inline Edit Part"), - precondition: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineEditVisible), - kbOpts: { - weight: KeybindingWeight.EditorContrib + 1, - kbExpr: ContextKeyExpr.and(EditorContextKeys.writable, InlineCompletionContextKeys.inlineEditVisible), - }, - }); - } - - public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { - const controller = InlineCompletionsController.get(editor); - await controller?.model.get()?.acceptNextInlineEditPart(controller.editor); - } -} - export class HideInlineCompletion extends EditorAction { public static ID = hideInlineCompletionId; @@ -368,7 +350,7 @@ export class DevExtractReproSample extends EditorAction { id: 'editor.action.inlineSuggest.dev.extractRepro', label: nls.localize('action.inlineSuggest.dev.extractRepro', "Developer: Extract Inline Suggest State"), alias: 'Developer: Inline Suggest Extract Repro', - precondition: InlineCompletionContextKeys.inlineEditVisible, + precondition: ContextKeyExpr.or(InlineCompletionContextKeys.inlineEditVisible, InlineCompletionContextKeys.inlineSuggestionVisible), }); } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 1658944cc6d..e124b19e2b3 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -65,7 +65,7 @@ export class InlineCompletionsController extends Disposable { private readonly _suggestWidgetAdapter = this._register(new ObservableSuggestWidgetAdapter( this._editorObs, item => this.model.get()?.handleSuggestAccepted(item), - () => this.model.get()?.selectedInlineCompletion.get()?.toSingleTextEdit(undefined), + () => this.model.get()?.selectedInlineCompletion.get()?.getSingleTextEdit(), )); private readonly _enabledInConfig = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).enabled); @@ -215,7 +215,7 @@ export class InlineCompletionsController extends Disposable { const model = this.model.get(); if (!model) { return; } - if (model.state.get()?.inlineCompletion?.request.isExplicitRequest && model.inlineEditAvailable.get()) { + if (model.state.get()?.inlineCompletion?.isFromExplicitRequest && model.inlineEditAvailable.get()) { // dont hide inline edits on blur when requested explicitly return; } @@ -262,8 +262,8 @@ export class InlineCompletionsController extends Disposable { await timeout(50, cancelOnDispose(store)); await waitForState(this._suggestWidgetAdapter.selectedItem, isUndefined, () => false, cancelOnDispose(store)); + await this._accessibilitySignalService.playSignal(state.kind === 'ghostText' ? AccessibilitySignal.inlineSuggestion : AccessibilitySignal.nextEditSuggestion); - await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { if (state.kind === 'ghostText') { this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); @@ -285,7 +285,7 @@ export class InlineCompletionsController extends Disposable { this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.cursorInIndentation, this._cursorIsInIndentation)); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.hasSelection, reader => !this._editorObs.cursorSelection.read(reader)?.isEmpty())); - this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.cursorAtInlineEdit, this.model.map((m, reader) => m?.inlineEditState?.read(reader)?.cursorAtInlineEdit))); + this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.cursorAtInlineEdit, this.model.map((m, reader) => m?.inlineEditState?.read(reader)?.cursorAtInlineEdit.read(reader)))); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.tabShouldAcceptInlineEdit, this.model.map((m, r) => !!m?.tabShouldAcceptInlineEdit.read(r)))); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.tabShouldJumpToInlineEdit, this.model.map((m, r) => !!m?.tabShouldJumpToInlineEdit.read(r)))); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineEditVisible, reader => this.model.read(reader)?.inlineEditState.read(reader) !== undefined)); @@ -298,7 +298,7 @@ export class InlineCompletionsController extends Disposable { this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.suppressSuggestions, reader => { const model = this.model.read(reader); const state = model?.inlineCompletionState.read(reader); - return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.source.inlineCompletions.suppressSuggestions : undefined; + return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.source.inlineSuggestions.suppressSuggestions : undefined; })); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineSuggestionVisible, reader => { const model = this.model.read(reader); @@ -350,4 +350,8 @@ export class InlineCompletionsController extends Disposable { m.jump(); } } + + public testOnlyDisableUi() { + this._view.dispose(); + } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index 5bf486b8775..06a0b9d9204 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -8,7 +8,7 @@ import { registerAction2 } from '../../../../platform/actions/common/actions.js' import { wrapInHotClass1 } from '../../../../platform/observable/common/wrapInHotClass.js'; import { EditorContributionInstantiation, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { HoverParticipantRegistry } from '../../hover/browser/hoverTypes.js'; -import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, DevExtractReproSample, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, ExplicitTriggerInlineEditAction, TriggerInlineSuggestionAction, TriggerInlineEditAction, ToggleInlineCompletionShowCollapsed, AcceptNextInlineEditPart } from './controller/commands.js'; +import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, DevExtractReproSample, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, ExplicitTriggerInlineEditAction, TriggerInlineSuggestionAction, TriggerInlineEditAction, ToggleInlineCompletionShowCollapsed } from './controller/commands.js'; import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; import { InlineCompletionsHoverParticipant } from './hintsWidget/hoverParticipant.js'; import { InlineCompletionsAccessibleView } from './inlineCompletionsAccessibleView.js'; @@ -29,7 +29,6 @@ registerEditorAction(AcceptNextLineOfInlineCompletion); registerEditorAction(AcceptInlineCompletion); registerEditorAction(ToggleInlineCompletionShowCollapsed); registerEditorAction(HideInlineCompletion); -registerEditorAction(AcceptNextInlineEditPart); registerEditorAction(JumpToNextInlineEdit); registerAction2(ToggleAlwaysShowInlineSuggestionToolbar); registerEditorAction(DevExtractReproSample); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 959a99ea366..a47deb89b84 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -17,7 +17,6 @@ import { InlineCompletionsModel } from './model/inlineCompletionsModel.js'; import { TextEdit } from '../../../common/core/textEdit.js'; import { LineEdit } from '../../../common/core/lineEdit.js'; import { TextModelText } from '../../../common/model/textModelText.js'; -import { localize } from '../../../../nls.js'; export class InlineCompletionsAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; @@ -43,16 +42,17 @@ export class InlineCompletionsAccessibleView implements IAccessibleViewImplement class InlineCompletionsAccessibleViewContentProvider extends Disposable implements IAccessibleViewContentProvider { private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + public readonly options: { language: string | undefined; type: AccessibleViewType.View }; constructor( private readonly _editor: ICodeEditor, private readonly _model: InlineCompletionsModel, ) { super(); + this.options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; } public readonly id = AccessibleViewProviderId.InlineCompletions; public readonly verbositySettingKey = 'accessibility.verbosity.inlineCompletions'; - public readonly options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; public provideContent(): string { const state = this._model.state.get(); @@ -70,7 +70,7 @@ class InlineCompletionsAccessibleViewContentProvider extends Disposable implemen } else { const text = new TextModelText(this._model.textModel); const lineEdit = LineEdit.fromTextEdit(new TextEdit(state.edits), text); - return localize('inlineEditAvailable', 'There is an inline edit available:') + '\n' + lineEdit.humanReadablePatch(text.getLines()); + return lineEdit.humanReadablePatch(text.getLines()); } } public provideNextContent(): string | undefined { diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts index 91d80882aaf..34f134540a6 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts @@ -8,10 +8,42 @@ import { autorunWithStore } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { CodeEditorWidget } from '../../../../browser/widget/codeEditor/codeEditorWidget.js'; -import { IRecordableEditorLogEntry, StructuredLogger } from '../structuredLogger.js'; +import { IDocumentEventDataSetChangeReason, IRecordableEditorLogEntry, StructuredLogger } from '../structuredLogger.js'; + +export interface ITextModelChangeRecorderMetadata { + source?: string; + extensionId?: string; + nes?: boolean; + type?: 'word' | 'line'; +} export class TextModelChangeRecorder extends Disposable { - private readonly _structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast(), + private static _nextMetadataId = 0; + private static _metaDataMap = new Map(); + + /** + * Adds metadata to any edit operation made in the callback (sync). + */ + public static editWithMetadata(metadata: ITextModelChangeRecorderMetadata, cb: () => T): T { + const id = this._nextMetadataId++; + this._metaDataMap.set(id, metadata); + try { + const result = cb(); + return result; + } finally { + this._metaDataMap.delete(id); + } + } + + private static _getCurrentMetadata(): ITextModelChangeRecorderMetadata { + const result: ITextModelChangeRecorderMetadata = {}; + for (const metadata of this._metaDataMap.values()) { + Object.assign(result, metadata); + } + return result; + } + + private readonly _structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast(), 'editor.inlineSuggest.logChangeReason.commandId' )); @@ -35,19 +67,28 @@ export class TextModelChangeRecorder extends Disposable { store.add(this._editor.onDidChangeModelContent(e => { const tm = this._editor.getModel(); if (!tm) { return; } + const metadata = TextModelChangeRecorder._getCurrentMetadata(); + if (sources.length === 0 && metadata.source) { + sources.push(metadata.source); + } + for (const source of sources) { - const data: IRecordableEditorLogEntry & { source: string } = { + const data: IRecordableEditorLogEntry & IDocumentEventDataSetChangeReason = { + ...metadata, sourceId: 'TextModel.setChangeReason', source: source, time: Date.now(), modelUri: tm.uri.toString(), modelVersion: tm.getVersionId(), }; - this._structuredLogger.log(data); + setTimeout(() => { + // To ensure that this reaches the extension host after the content change event. + // (Without the setTimeout, I observed this command being called before the content change event arrived) + this._structuredLogger.log(data); + }, 0); } sources.length = 0; })); - })); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index 700bc76bf64..26fd510b518 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/code/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/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 43be28ea2d5..b92e6b7ba44 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -8,7 +8,7 @@ import { itemsEquals } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, constObservable, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, autorunWithStore, constObservable, derived, derivedHandleChanges, derivedOpts, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { commonPrefixLength, firstNonWhitespaceIndex } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -26,20 +26,22 @@ import { Selection } from '../../../../common/core/selection.js'; import { SingleTextEdit, TextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; -import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; +import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; +import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; -import { addPositions, getEndPositionsAfterApplying, getModifiedRangesAfterApplying, substringPos, subtractPositions } from '../utils.js'; +import { addPositions, getEndPositionsAfterApplying, substringPos, subtractPositions } from '../utils.js'; import { AnimatedValue, easeOutCubic, ObservableAnimatedValue } from './animation.js'; +import { ITextModelChangeRecorderMetadata, TextModelChangeRecorder } from './changeRecorder.js'; import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; -import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from './inlineCompletionsSource.js'; +import { InlineCompletionsSource } from './inlineCompletionsSource.js'; import { InlineEdit } from './inlineEdit.js'; -import { InlineCompletionItem } from './provideInlineCompletions.js'; +import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem } from './inlineSuggestionItem.js'; import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; import { SuggestItemInfo } from './suggestWidgetAdapter.js'; @@ -50,6 +52,8 @@ export class InlineCompletionsModel extends Disposable { private readonly _forceUpdateExplicitlySignal = observableSignal(this); private readonly _noDelaySignal = observableSignal(this); + private readonly _fetchSpecificProviderSignal = observableSignal(this); + // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); @@ -80,36 +84,18 @@ export class InlineCompletionsModel extends Disposable { @ICommandService private readonly _commandService: ICommandService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, ) { super(); this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise)); - let lastItem: InlineCompletionWithUpdatedRange | undefined = undefined; this._register(autorun(reader => { /** @description call handleItemDidShow */ const item = this.inlineCompletionState.read(reader); const completion = item?.inlineCompletion; - if (completion?.semanticId !== lastItem?.semanticId) { - lastItem = completion; - if (completion) { - const i = completion.inlineCompletion; - const src = i.source; - src.provider.handleItemDidShow?.(src.inlineCompletions, i.sourceInlineCompletion, i.insertText); - } - } - })); - this._register(autorun(reader => { - /** @description handle text edits collapsing */ - const inlineCompletions = this._source.inlineCompletions.read(reader); - if (!inlineCompletions) { - return; - } - for (const inlineCompletion of inlineCompletions.inlineCompletions) { - if (inlineCompletion.updatedEdit.read(reader) === undefined) { - this.stop(); - break; - } + if (completion) { + this.handleInlineSuggestionShown(completion); } })); @@ -119,7 +105,7 @@ export class InlineCompletionsModel extends Disposable { })); this._register(autorun(reader => { - const jumpToReset = this.state.map(s => !s || s.kind === 'inlineEdit' && !s.cursorAtInlineEdit).read(reader); + const jumpToReset = this.state.map((s, reader) => !s || s.kind === 'inlineEdit' && !s.cursorAtInlineEdit.read(reader)).read(reader); if (jumpToReset) { this._jumpedToId.set(undefined, undefined); } @@ -133,22 +119,52 @@ export class InlineCompletionsModel extends Disposable { this._editor.pushUndoStop(); this._lastShownInlineCompletionInfo = { alternateTextModelVersionId: this.textModel.getAlternativeVersionId(), - inlineCompletion: this.state.get()!.inlineCompletion!.inlineCompletion, + inlineCompletion: this.state.get()!.inlineCompletion!, }; } })); + const inlineCompletionProviders = observableFromEvent(this._languageFeaturesService.inlineCompletionsProvider.onDidChange, () => this._languageFeaturesService.inlineCompletionsProvider.all(textModel)); + this._register(autorunWithStore((reader, store) => { + const providers = inlineCompletionProviders.read(reader); + for (const provider of providers) { + if (!provider.onDidChangeInlineCompletions) { + continue; + } + + store.add(provider.onDidChangeInlineCompletions(() => { + if (!this._enabled.get()) { + return; + } + + // If there is an active suggestion from a different provider, we ignore the update + const activeState = this.state.get(); + if (activeState && (activeState.inlineCompletion || activeState.edits) && activeState.inlineCompletion?.source.provider !== provider) { + return; + } + + transaction(tx => { + this._fetchSpecificProviderSignal.trigger(tx, provider); + this.trigger(tx); + }); + + })); + } + })); + this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store); } - private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineCompletionItem } | undefined = undefined; - private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineCompletionItem } | undefined = undefined; + private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; + private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined; private readonly _didUndoInlineEdits = derivedHandleChanges({ owner: this, - createEmptyChangeSummary: () => ({ didUndo: false }), - handleChange: (ctx, changeSummary) => { - changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; - return true; + changeTracker: { + createChangeSummary: () => ({ didUndo: false }), + handleChange: (ctx, changeSummary) => { + changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing; + return true; + } } }, (reader, changeSummary) => { const versionId = this._textModelVersionId.read(reader); @@ -215,27 +231,30 @@ export class InlineCompletionsModel extends Disposable { private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ owner: this, - createEmptyChangeSummary: () => ({ - dontRefetch: false, - preserveCurrentCompletion: false, - inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, - onlyRequestInlineEdits: false, - shouldDebounce: true, - }), - handleChange: (ctx, changeSummary) => { - /** @description fetch inline completions */ - if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { - changeSummary.preserveCurrentCompletion = true; - } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { - changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; - } else if (ctx.didChange(this.dontRefetchSignal)) { - changeSummary.dontRefetch = true; - } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { - changeSummary.onlyRequestInlineEdits = true; - } else if (ctx.didChange(this._noDelaySignal)) { - changeSummary.shouldDebounce = false; - } - return true; + changeTracker: { + createChangeSummary: () => ({ + dontRefetch: false, + preserveCurrentCompletion: false, + inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic, + onlyRequestInlineEdits: false, + shouldDebounce: true, + provider: undefined as InlineCompletionsProvider | undefined, + }), + handleChange: (ctx, changeSummary) => { + /** @description fetch inline completions */ + if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) { + changeSummary.preserveCurrentCompletion = true; + } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { + changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } else if (ctx.didChange(this.dontRefetchSignal)) { + changeSummary.dontRefetch = true; + } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { + changeSummary.onlyRequestInlineEdits = true; + } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { + changeSummary.provider = ctx.change; + } + return true; + }, }, }, (reader, changeSummary) => { this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation @@ -243,6 +262,7 @@ export class InlineCompletionsModel extends Disposable { this.dontRefetchSignal.read(reader); this._onlyRequestInlineEditsSignal.read(reader); this._forceUpdateExplicitlySignal.read(reader); + this._fetchSpecificProviderSignal.read(reader); const shouldUpdate = (this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader); if (!shouldUpdate) { this._source.cancelUpdate(); @@ -254,14 +274,7 @@ export class InlineCompletionsModel extends Disposable { const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); const suggestItem = this._selectedSuggestItem.read(reader); if (suggestWidgetInlineCompletions && !suggestItem) { - const inlineCompletions = this._source.inlineCompletions.get(); - transaction(tx => { - /** @description Seed inline completions with (newer) suggest widget inline completions */ - if (!inlineCompletions || suggestWidgetInlineCompletions.request.versionId > inlineCompletions.request.versionId) { - this._source.inlineCompletions.set(suggestWidgetInlineCompletions.clone(), tx); - } - this._source.clearSuggestWidgetInlineCompletions(tx); - }); + this._source.seedInlineCompletionsWithSuggestWidget(); } const cursorPosition = this.primaryPosition.get(); @@ -269,7 +282,7 @@ export class InlineCompletionsModel extends Disposable { return Promise.resolve(true); } - if (this._didUndoInlineEdits.read(reader)) { + if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) { transaction(tx => { this._source.clear(tx); }); @@ -300,7 +313,9 @@ export class InlineCompletionsModel extends Disposable { ? itemToPreserveCandidate : undefined; const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); - return this._source.fetch(cursorPosition, context, itemToPreserve, changeSummary.shouldDebounce, userJumpedToActiveCompletion); + const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); + + return this._source.fetch(providers, cursorPosition, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider); }); public async trigger(tx?: ITransaction, options?: { onlyFetchInlineEdits?: boolean; noDelay?: boolean }): Promise { @@ -332,14 +347,11 @@ export class InlineCompletionsModel extends Disposable { subtransaction(tx, tx => { if (stopReason === 'explicitCancel') { const inlineCompletion = this.state.get()?.inlineCompletion; - const source = inlineCompletion?.source; - const sourceInlineCompletion = inlineCompletion?.sourceInlineCompletion; - if (sourceInlineCompletion && source?.provider.handleRejection) { - source.provider.handleRejection(source.inlineCompletions, sourceInlineCompletion); + if (inlineCompletion) { + inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected }); } } - this._inAcceptPartialFlow.set(false, tx); this._isActive.set(false, tx); this._source.clear(tx); }); @@ -349,11 +361,11 @@ export class InlineCompletionsModel extends Disposable { const c = this._source.inlineCompletions.read(reader); if (!c) { return undefined; } const cursorPosition = this.primaryPosition.read(reader); - let inlineEdit: InlineCompletionWithUpdatedRange | undefined = undefined; - const visibleCompletions: InlineCompletionWithUpdatedRange[] = []; + let inlineEdit: InlineEditItem | undefined = undefined; + const visibleCompletions: InlineCompletionItem[] = []; for (const completion of c.inlineCompletions) { - if (!completion.sourceInlineCompletion.isInlineEdit) { - if (completion.isVisible(this.textModel, cursorPosition, reader)) { + if (!completion.isInlineEdit) { + if (completion.isVisible(this.textModel, cursorPosition)) { visibleCompletions.push(completion); } } else { @@ -390,18 +402,18 @@ export class InlineCompletionsModel extends Disposable { return idx; }); - public readonly selectedInlineCompletion = derived(this, (reader) => { + public readonly selectedInlineCompletion = derived(this, (reader) => { const filteredCompletions = this._filteredInlineCompletionItems.read(reader); const idx = this.selectedInlineCompletionIndex.read(reader); return filteredCompletions[idx]; }); public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, - r => this.selectedInlineCompletion.read(r)?.source.inlineCompletions.commands ?? [] + r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] ); public readonly lastTriggerKind: IObservable - = this._source.inlineCompletions.map(this, v => v?.request.context.triggerKind); + = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind); public readonly inlineCompletionsCount = derived(this, reader => { if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) { @@ -419,13 +431,13 @@ export class InlineCompletionsModel extends Disposable { primaryGhostText: GhostTextOrReplacement; ghostTexts: readonly GhostTextOrReplacement[]; suggestItem: SuggestItemInfo | undefined; - inlineCompletion: InlineCompletionWithUpdatedRange | undefined; + inlineCompletion: InlineCompletionItem | undefined; } | { kind: 'inlineEdit'; edits: readonly SingleTextEdit[]; inlineEdit: InlineEdit; - inlineCompletion: InlineCompletionWithUpdatedRange; - cursorAtInlineEdit: boolean; + inlineCompletion: InlineEditItem; + cursorAtInlineEdit: IObservable; } | undefined>({ owner: this, equalsFn: (a, b) => { @@ -436,7 +448,7 @@ export class InlineCompletionsModel extends Disposable { && a.inlineCompletion === b.inlineCompletion && a.suggestItem === b.suggestItem; } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { - return a.inlineEdit.equals(b.inlineEdit) && a.cursorAtInlineEdit === b.cursorAtInlineEdit; + return a.inlineEdit.equals(b.inlineEdit); } return false; } @@ -449,21 +461,15 @@ export class InlineCompletionsModel extends Disposable { if (this._hasVisiblePeekWidgets.read(reader)) { return undefined; } - let edit = inlineEditResult.toSingleTextEdit(reader); + let edit = inlineEditResult.getSingleTextEdit(); edit = singleTextRemoveCommonPrefix(edit, model); - const cursorPos = this.primaryPosition.read(reader); - const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber); - const cursorInsideShowRange = cursorAtInlineEdit || (inlineEditResult.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true); - - if (!cursorInsideShowRange && !this._inAcceptFlow.read(reader)) { - return undefined; - } + const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); - const commands = inlineEditResult.inlineCompletion.source.inlineCompletions.commands; - const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult.inlineCompletion); + const commands = inlineEditResult.source.inlineSuggestions.commands; + const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); - const edits = inlineEditResult.updatedEdit.read(reader); + const edits = inlineEditResult.updatedEdit; const e = edits ? TextEdit.fromOffsetEdit(edits, new TextModelText(this.textModel)).edits : [edit]; return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit }; @@ -471,7 +477,7 @@ export class InlineCompletionsModel extends Disposable { const suggestItem = this._selectedSuggestItem.read(reader); if (suggestItem) { - const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.toSingleTextEdit(), model); + const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model); const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader); @@ -493,7 +499,7 @@ export class InlineCompletionsModel extends Disposable { const inlineCompletion = this.selectedInlineCompletion.read(reader); if (!inlineCompletion) { return undefined; } - const replacement = inlineCompletion.toSingleTextEdit(reader); + const replacement = inlineCompletion.getSingleTextEdit(); const mode = this._inlineSuggestMode.read(reader); const positions = this._positions.read(reader); const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; @@ -541,11 +547,11 @@ export class InlineCompletionsModel extends Disposable { const model = this.textModel; const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader); const candidateInlineCompletions = suggestWidgetInlineCompletions - ? suggestWidgetInlineCompletions.inlineCompletions + ? suggestWidgetInlineCompletions.inlineCompletions.filter(c => !c.isInlineEdit) : [this.selectedInlineCompletion.read(reader)].filter(isDefined); const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => { - let r = completion.toSingleTextEdit(reader); + let r = completion.getSingleTextEdit(); r = singleTextRemoveCommonPrefix( r, model, @@ -558,7 +564,7 @@ export class InlineCompletionsModel extends Disposable { } public readonly warning = derived(this, reader => { - return this.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.warning; + return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; }); public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { @@ -583,6 +589,10 @@ export class InlineCompletionsModel extends Disposable { return false; } + if (state.inlineCompletion.displayLocation) { + return false; + } + const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId @@ -629,7 +639,7 @@ export class InlineCompletionsModel extends Disposable { return true; } - return !s.cursorAtInlineEdit; + return !s.cursorAtInlineEdit.read(reader); }); public readonly tabShouldAcceptInlineEdit = derived(this, reader => { @@ -640,7 +650,7 @@ export class InlineCompletionsModel extends Disposable { if (this.showCollapsed.read(reader)) { return false; } - if (s.inlineEdit.range.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { + if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { return true; } if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { @@ -650,7 +660,7 @@ export class InlineCompletionsModel extends Disposable { return false; } - return s.cursorAtInlineEdit; + return s.cursorAtInlineEdit.read(reader); }); private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise { @@ -669,32 +679,34 @@ export class InlineCompletionsModel extends Disposable { public async previous(): Promise { await this._deltaSelectedInlineCompletionIndex(-1); } + private _getMetadata(completion: InlineSuggestionItem, type: 'word' | 'line' | undefined = undefined): ITextModelChangeRecorderMetadata { + return { + extensionId: completion.source.provider.groupId, + nes: completion.isInlineEdit, + type + }; + } + public async accept(editor: ICodeEditor = this._editor): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } - if (this._inAcceptPartialFlow.get()) { - this._inAcceptPartialFlow.set(false, undefined); - this.jump(); - return; - } - - let completionWithUpdatedRange: InlineCompletionWithUpdatedRange; + let completion: InlineSuggestionItem; const state = this.state.get(); if (state?.kind === 'ghostText') { if (!state || state.primaryGhostText.isEmpty() || !state.inlineCompletion) { return; } - completionWithUpdatedRange = state.inlineCompletion; + completion = state.inlineCompletion; } else if (state?.kind === 'inlineEdit') { - completionWithUpdatedRange = state.inlineCompletion; + completion = state.inlineCompletion; } else { return; } - const completion = completionWithUpdatedRange.toInlineCompletion(undefined); + completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted }); if (completion.command) { // Make sure the completion list will not be disposed. @@ -703,23 +715,31 @@ export class InlineCompletionsModel extends Disposable { editor.pushUndoStop(); if (completion.snippetInfo) { - editor.executeEdits( - 'inlineSuggestion.accept', - [ - EditOperation.replace(completion.range, ''), - ...completion.additionalTextEdits - ] - ); + TextModelChangeRecorder.editWithMetadata(this._getMetadata(completion), () => { + editor.executeEdits( + 'inlineSuggestion.accept', + [ + EditOperation.replace(completion.editRange, ''), + ...completion.additionalTextEdits + ] + ); + }); editor.setPosition(completion.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept'); SnippetController2.get(editor)?.insert(completion.snippetInfo.snippet, { undoStopBefore: false }); } else { const edits = state.edits; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); - editor.executeEdits('inlineSuggestion.accept', [ - ...edits.map(edit => EditOperation.replace(edit.range, edit.text)), - ...completion.additionalTextEdits - ]); - editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept'); + + TextModelChangeRecorder.editWithMetadata(this._getMetadata(completion), () => { + editor.executeEdits('inlineSuggestion.accept', [ + ...edits.map(edit => EditOperation.replace(edit.range, edit.text)), + ...completion.additionalTextEdits + ]); + }); + if (completion.displayLocation === undefined) { + // do not move the cursor when the completion is displayed in a different location + editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept'); + } if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) { // we can assume that edits is sorted! @@ -746,8 +766,8 @@ export class InlineCompletionsModel extends Disposable { this._lastAcceptedInlineCompletionInfo = { textModelVersionIdAfter: this.textModel.getVersionId(), inlineCompletion: completion }; } - public async acceptNextWord(editor: ICodeEditor): Promise { - await this._acceptNext(editor, (pos, text) => { + public async acceptNextWord(): Promise { + await this._acceptNext(this._editor, 'word', (pos, text) => { const langId = this.textModel.getLanguageIdAtPosition(pos.lineNumber, pos.column); const config = this._languageConfigurationService.getLanguageConfiguration(langId); const wordRegExp = new RegExp(config.wordDefinition.source, config.wordDefinition.flags.replace('g', '')); @@ -775,8 +795,8 @@ export class InlineCompletionsModel extends Disposable { }, PartialAcceptTriggerKind.Word); } - public async acceptNextLine(editor: ICodeEditor): Promise { - await this._acceptNext(editor, (pos, text) => { + public async acceptNextLine(): Promise { + await this._acceptNext(this._editor, 'line', (pos, text) => { const m = text.match(/\n/); if (m && m.index !== undefined) { return m.index + 1; @@ -785,7 +805,7 @@ export class InlineCompletionsModel extends Disposable { }, PartialAcceptTriggerKind.Line); } - private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { + private async _acceptNext(editor: ICodeEditor, type: 'word' | 'line', getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -795,9 +815,9 @@ export class InlineCompletionsModel extends Disposable { return; } const ghostText = state.primaryGhostText; - const completion = state.inlineCompletion.toInlineCompletion(undefined); + const completion = state.inlineCompletion; - if (completion.snippetInfo || completion.filterText !== completion.insertText) { + if (completion.snippetInfo) { // not in WYSIWYG mode, partial commit might change completion, thus it is not supported await this.accept(editor); return; @@ -827,122 +847,47 @@ export class InlineCompletionsModel extends Disposable { const primaryEdit = new SingleTextEdit(replaceRange, newText); const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)]; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); - editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text))); + TextModelChangeRecorder.editWithMetadata(this._getMetadata(completion, type), () => { + editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text))); + }); editor.setSelections(selections, 'inlineCompletionPartialAccept'); editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate); } finally { this._isAcceptingPartially = false; } - if (completion.source.provider.handlePartialAccept) { - const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); - // This assumes that the inline completion and the model use the same EOL style. - const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); - const acceptedLength = text.length; - completion.source.provider.handlePartialAccept( - completion.source.inlineCompletions, - completion.sourceInlineCompletion, - acceptedLength, - { kind, acceptedLength: acceptedLength, } - ); - } - } finally { - completion.source.removeRef(); - } - } - - // TODO: clean this up if we keep it - private readonly _inAcceptPartialFlow = observableValue(this, false); - public readonly inPartialAcceptFlow: IObservable = this._inAcceptPartialFlow; - public async acceptNextInlineEditPart(editor: ICodeEditor): Promise { - if (editor.getModel() !== this.textModel) { - throw new BugIndicatingError(); - } + const acceptedRange = Range.fromPositions(completion.editRange.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); + // This assumes that the inline completion and the model use the same EOL style. + const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); + const acceptedLength = text.length; + completion.reportPartialAccept(acceptedLength, { kind, acceptedLength: acceptedLength }); - const state = this.inlineEditState.get(); - const updatedEdit = state?.inlineCompletion.updatedEdit.get(); - const completion = state?.inlineCompletion.toInlineCompletion(undefined); - if (!updatedEdit || updatedEdit.isEmpty || !completion) { - return; - } - - const nextPart = updatedEdit.edits[0]; - - const edit = new SingleTextEdit(Range.fromPositions( - this.textModel.getPositionAt(nextPart.replaceRange.start), - this.textModel.getPositionAt(nextPart.replaceRange.endExclusive) - ), nextPart.newText); - - const cursorAtStartPosition = this._editor.getSelection()?.getStartPosition().equals(edit.range.getStartPosition()); - if (!cursorAtStartPosition || !this._inAcceptPartialFlow.get()) { - this._inAcceptPartialFlow.set(true, undefined); - this.jump(); - return; - } - - const partToJumpToNext = updatedEdit.edits[1] ?? undefined; - const editToJumpToNext = partToJumpToNext ? new SingleTextEdit(Range.fromPositions( - this.textModel.getPositionAt(partToJumpToNext.replaceRange.start), - this.textModel.getPositionAt(partToJumpToNext.replaceRange.endExclusive) - ), partToJumpToNext.newText) : undefined; - - // Executing the edit might free the completion, so we have to hold a reference on it. - completion.source.addRef(); - try { - this._isAcceptingPartially = true; - try { - editor.pushUndoStop(); - - let selections; - if (editToJumpToNext) { - const [_, rangeOfEditToJumpTo] = getModifiedRangesAfterApplying([edit, editToJumpToNext]); - selections = [Selection.fromPositions(rangeOfEditToJumpTo.getStartPosition())]; - } else { - selections = getEndPositionsAfterApplying([edit]).map(p => Selection.fromPositions(p)); - } - - const edits = [edit]; - editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text))); - editor.setSelections(selections, 'inlineCompletionPartialAccept'); - editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate); - } finally { - this._isAcceptingPartially = false; - } } finally { completion.source.removeRef(); } } public handleSuggestAccepted(item: SuggestItemInfo) { - const itemEdit = singleTextRemoveCommonPrefix(item.toSingleTextEdit(), this.textModel); + const itemEdit = singleTextRemoveCommonPrefix(item.getSingleTextEdit(), this.textModel); const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); if (!augmentedCompletion) { return; } - const source = augmentedCompletion.completion.source; - const sourceInlineCompletion = augmentedCompletion.completion.sourceInlineCompletion; - - const completion = augmentedCompletion.completion.toInlineCompletion(undefined); // This assumes that the inline completion and the model use the same EOL style. - const alreadyAcceptedLength = this.textModel.getValueInRange(completion.range, EndOfLinePreference.LF).length; + const alreadyAcceptedLength = this.textModel.getValueInRange(augmentedCompletion.completion.editRange, EndOfLinePreference.LF).length; const acceptedLength = alreadyAcceptedLength + itemEdit.text.length; - source.provider.handlePartialAccept?.( - source.inlineCompletions, - sourceInlineCompletion, - itemEdit.text.length, - { - kind: PartialAcceptTriggerKind.Suggest, - acceptedLength, - } - ); + augmentedCompletion.completion.reportPartialAccept(itemEdit.text.length, { + kind: PartialAcceptTriggerKind.Suggest, + acceptedLength, + }); } public extractReproSample(): Repro { const value = this.textModel.getValue(); - const item = this.state.get()?.inlineCompletion?.toInlineCompletion(undefined); + const item = this.state.get()?.inlineCompletion; return { documentValue: value, - inlineCompletion: item?.sourceInlineCompletion, + inlineCompletion: item?.getSourceCompletion(), }; } @@ -957,15 +902,16 @@ export class InlineCompletionsModel extends Disposable { transaction(tx => { this._jumpedToId.set(s.inlineCompletion.semanticId, tx); this.dontRefetchSignal.trigger(tx); - const edit = s.inlineCompletion.toSingleTextEdit(undefined); - this._editor.setPosition(edit.range.getStartPosition(), 'inlineCompletions.jump'); + const targetRange = s.inlineCompletion.targetRange; + const targetPosition = targetRange.getStartPosition(); + this._editor.setPosition(targetPosition, 'inlineCompletions.jump'); // TODO: consider using view information to reveal it - const isSingleLineChange = edit.range.startLineNumber === edit.range.endLineNumber && !edit.text.includes('\n'); + const isSingleLineChange = targetRange.isSingleLine() && (s.inlineCompletion.displayLocation || !s.inlineCompletion.insertText.includes('\n')); if (isSingleLineChange) { - this._editor.revealPosition(edit.range.getStartPosition()); + this._editor.revealPosition(targetPosition); } else { - const revealRange = new Range(edit.range.startLineNumber - 1, 1, edit.range.endLineNumber + 1, 1); + const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1); this._editor.revealRange(revealRange, ScrollType.Immediate); } @@ -973,17 +919,8 @@ export class InlineCompletionsModel extends Disposable { }); } - public async handleInlineEditShown(inlineCompletion: InlineCompletionItem): Promise { - if (inlineCompletion.didShow) { - return; - } - inlineCompletion.markAsShown(); - - inlineCompletion.source.provider.handleItemDidShow?.(inlineCompletion.source.inlineCompletions, inlineCompletion.sourceInlineCompletion, inlineCompletion.insertText); - - if (inlineCompletion.shownCommand) { - await this._commandService.executeCommand(inlineCompletion.shownCommand.id, ...(inlineCompletion.shownCommand.arguments || [])); - } + public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem): Promise { + await inlineCompletion.reportInlineEditShown(this._commandService); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index afaad779d6d..9ff5450279c 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -5,41 +5,34 @@ import { compareUndefinedSmallest, numberComparator } from '../../../../../base/common/arrays.js'; import { findLastMax } from '../../../../../base/common/arraysFind.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; -import { BugIndicatingError } from '../../../../../base/common/errors.js'; -import { matchesSubString } from '../../../../../base/common/filters.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IObservableWithChange, IReader, ITransaction, derived, derivedHandleChanges, disposableObservableValue, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; +import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChanges, transaction } from '../../../../../base/common/observable.js'; +// eslint-disable-next-line local/code-no-deep-import-of-internal +import { observableReducerSettable } from '../../../../../base/common/observableInternal/reducer.js'; +import { isDefined } from '../../../../../base/common/types.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'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; -import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { OffsetEdit } from '../../../../common/core/offsetEdit.js'; import { Position } from '../../../../common/core/position.js'; -import { Range } from '../../../../common/core/range.js'; -import { SingleTextEdit, StringText } from '../../../../common/core/textEdit.js'; -import { TextLength } from '../../../../common/core/textLength.js'; -import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; -import { InlineCompletionContext, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReasonKind, InlineCompletionContext, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; -import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; +import { ITextModel } from '../../../../common/model.js'; import { OffsetEdits } from '../../../../common/model/textModelOffsetEdit.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; -import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; -import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; -import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; -import { StructuredLogger, IRecordableEditorLogEntry, IRecordableLogEntry, formatRecordableLogEntry } from '../structuredLogger.js'; +import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js'; +import { wait } from '../utils.js'; +import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js'; +import { InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; export class InlineCompletionsSource extends Disposable { private static _requestId = 0; private readonly _updateOperation = this._register(new MutableDisposable()); - public readonly inlineCompletions = this._register(disposableObservableValue('inlineCompletions', undefined)); - public readonly suggestWidgetInlineCompletions = this._register(disposableObservableValue('suggestWidgetInlineCompletions', undefined)); private readonly _loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); @@ -50,11 +43,41 @@ export class InlineCompletionsSource extends Disposable { 'editor.inlineSuggest.logFetch.commandId' )); + private readonly _state = observableReducerSettable(this, { + initial: () => ({ + inlineCompletions: InlineCompletionsState.createEmpty(), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }), + disposeFinal: (values) => { + values.inlineCompletions.dispose(); + values.suggestWidgetInlineCompletions.dispose(); + }, + changeTracker: recordChanges({ versionId: this._versionId }), + update: (reader, previousValue, changes) => { + const edit = OffsetEdit.join(changes.changes.map(c => c.change ? OffsetEdits.fromContentChanges(c.change.changes) : OffsetEdit.empty).filter(isDefined)); + + if (edit.isEmpty) { + return previousValue; + } + try { + return { + inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel), + }; + } finally { + previousValue.inlineCompletions.dispose(); + previousValue.suggestWidgetInlineCompletions.dispose(); + } + } + }); + + public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); + public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); + constructor( private readonly _textModel: ITextModel, private readonly _versionId: IObservableWithChange, private readonly _debounceValue: IFeatureDebounceInformation, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -84,14 +107,14 @@ export class InlineCompletionsSource extends Disposable { private readonly _loadingCount = observableValue(this, 0); public readonly loading = this._loadingCount.map(this, v => v > 0); - public fetch(position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineCompletionWithUpdatedRange | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable): Promise { + public fetch(providers: InlineCompletionsProvider[], position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable, providerhasChangedCompletion: boolean): Promise { const request = new UpdateRequest(position, context, this._textModel.getVersionId()); - const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions : this.inlineCompletions; + const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get(); - if (this._updateOperation.value?.request.satisfies(request)) { + if (!providerhasChangedCompletion && this._updateOperation.value?.request.satisfies(request)) { return this._updateOperation.value.promise; - } else if (target.get()?.request.satisfies(request)) { + } else if (target?.request?.satisfies(request)) { return Promise.resolve(true); } @@ -105,7 +128,7 @@ export class InlineCompletionsSource extends Disposable { try { const recommendedDebounceValue = this._debounceValue.get(this._textModel); const debounceValue = findLastMax( - this._languageFeaturesService.inlineCompletionsProvider.all(this._textModel).map(p => p.debounceDelayMs), + providers.map(p => p.debounceDelayMs), compareUndefinedSmallest(numberComparator) ) ?? recommendedDebounceValue; @@ -126,11 +149,11 @@ export class InlineCompletionsSource extends Disposable { } const startTime = new Date(); - let updatedCompletions: InlineCompletionProviderResult | undefined = undefined; + let providerResult: InlineCompletionProviderResult | undefined = undefined; let error: any = undefined; try { - updatedCompletions = await provideInlineCompletions( - this._languageFeaturesService.inlineCompletionsProvider, + providerResult = await provideInlineCompletions( + providers, position, this._textModel, context, @@ -145,7 +168,7 @@ export class InlineCompletionsSource extends Disposable { if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) { error = 'canceled'; } - const result = updatedCompletions?.completions.map(c => ({ + const result = providerResult?.completions.map(c => ({ range: c.range.toString(), text: c.insertText, isInlineEdit: !!c.isInlineEdit, @@ -156,37 +179,32 @@ export class InlineCompletionsSource extends Disposable { } if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId || userJumpedToActiveCompletion.get() /* In the meantime the user showed interest for the active completion so dont hide it */) { - updatedCompletions.dispose(); - return false; - } - - // Reuse Inline Edit if possible - if (activeInlineCompletion && activeInlineCompletion.isInlineEdit && activeInlineCompletion.updatedEditModelVersion === this._textModel.getVersionId() && ( - activeInlineCompletion.canBeReused(this._textModel, position) - || updatedCompletions.has(activeInlineCompletion.inlineCompletion) /* Inline Edit wins over completions if it's already been shown*/ - || updatedCompletions.isEmpty() /* Incoming completion is empty, keep the current one alive */ - )) { - activeInlineCompletion.reuse(); - updatedCompletions.dispose(); + providerResult.dispose(); return false; } const endTime = new Date(); this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime()); - // Reuse Inline Completion if possible - const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId); - if (activeInlineCompletion && !activeInlineCompletion.isInlineEdit && activeInlineCompletion.canBeReused(this._textModel, position)) { - const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined); - if (!updatedCompletions.has(asInlineCompletion)) { - completions.prepend(activeInlineCompletion.inlineCompletion, asInlineCompletion.range, true); - } - } - this._updateOperation.clear(); transaction(tx => { + const v = this._state.get(); /** @description Update completions with provider result */ - target.set(completions, tx); + if (context.selectedSuggestionInfo) { + this._state.set({ + inlineCompletions: InlineCompletionsState.createEmpty(), + suggestWidgetInlineCompletions: v.suggestWidgetInlineCompletions.createStateWithAppliedResults(providerResult, request, this._textModel, activeInlineCompletion), + }, tx); + } else { + this._state.set({ + inlineCompletions: v.inlineCompletions.createStateWithAppliedResults(providerResult, request, this._textModel, activeInlineCompletion), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }, tx); + } + + providerResult.dispose(); + v.inlineCompletions.dispose(); + v.suggestWidgetInlineCompletions.dispose(); }); } finally { @@ -204,15 +222,41 @@ export class InlineCompletionsSource extends Disposable { public clear(tx: ITransaction): void { this._updateOperation.clear(); - this.inlineCompletions.set(undefined, tx); - this.suggestWidgetInlineCompletions.set(undefined, tx); + const v = this._state.get(); + this._state.set({ + inlineCompletions: InlineCompletionsState.createEmpty(), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty() + }, tx); + v.inlineCompletions.dispose(); + v.suggestWidgetInlineCompletions.dispose(); + } + + public seedInlineCompletionsWithSuggestWidget(): void { + const inlineCompletions = this.inlineCompletions.get(); + const suggestWidgetInlineCompletions = this.suggestWidgetInlineCompletions.get(); + if (!suggestWidgetInlineCompletions) { + return; + } + transaction(tx => { + /** @description Seed inline completions with (newer) suggest widget inline completions */ + if (!inlineCompletions || (suggestWidgetInlineCompletions.request?.versionId ?? -1) > (inlineCompletions.request?.versionId ?? -1)) { + inlineCompletions?.dispose(); + const s = this._state.get(); + this._state.set({ + inlineCompletions: suggestWidgetInlineCompletions.clone(), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }, tx); + s.inlineCompletions.dispose(); + s.suggestWidgetInlineCompletions.dispose(); + } + this.clearSuggestWidgetInlineCompletions(tx); + }); } public clearSuggestWidgetInlineCompletions(tx: ITransaction): void { if (this._updateOperation.value?.request.context.selectedSuggestionInfo) { this._updateOperation.clear(); } - this.suggestWidgetInlineCompletions.set(undefined, tx); } public cancelUpdate(): void { @@ -220,23 +264,6 @@ export class InlineCompletionsSource extends Disposable { } } -function wait(ms: number, cancellationToken?: CancellationToken): Promise { - return new Promise(resolve => { - let d: IDisposable | undefined = undefined; - const handle = setTimeout(() => { - if (d) { d.dispose(); } - resolve(); - }, ms); - if (cancellationToken) { - d = cancellationToken.onCancellationRequested(() => { - clearTimeout(handle); - if (d) { d.dispose(); } - resolve(); - }); - } - }); -} - class UpdateRequest { constructor( public readonly position: Position, @@ -271,531 +298,71 @@ class UpdateOperation implements IDisposable { } } -export class UpToDateInlineCompletions implements IDisposable { - private readonly _inlineCompletions: InlineCompletionWithUpdatedRange[]; - public get inlineCompletions(): ReadonlyArray { return this._inlineCompletions; } - - private _refCount = 1; - private readonly _prependedInlineCompletionItems: InlineCompletionItem[] = []; - - constructor( - private readonly inlineCompletionProviderResult: InlineCompletionProviderResult, - public readonly request: UpdateRequest, - private readonly _textModel: ITextModel, - private readonly _versionId: IObservableWithChange, - ) { - this._inlineCompletions = inlineCompletionProviderResult.completions.map( - completion => new InlineCompletionWithUpdatedRange(completion, undefined, this._textModel, this._versionId, this.request) - ); - } - - public clone(): this { - this._refCount++; - return this; - } - - public dispose(): void { - this._refCount--; - if (this._refCount === 0) { - this.inlineCompletionProviderResult.dispose(); - for (const i of this._prependedInlineCompletionItems) { - i.source.removeRef(); - } - this._inlineCompletions.forEach(i => i.dispose()); - } - } - - public prepend(inlineCompletion: InlineCompletionItem, range: Range, addRefToSource: boolean): void { - if (addRefToSource) { - inlineCompletion.source.addRef(); - } - - this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, range, this._textModel, this._versionId, this.request)); - this._prependedInlineCompletionItems.push(inlineCompletion); - } -} - -export class InlineCompletionWithUpdatedRange extends Disposable { - public readonly semanticId = JSON.stringify([ - this.inlineCompletion.filterText, - this.inlineCompletion.insertText, - this.inlineCompletion.range.getStartPosition().toString() - ]); - - public get forwardStable() { - return this.source.inlineCompletions.enableForwardStability ?? false; +class InlineCompletionsState extends Disposable { + public static createEmpty(): InlineCompletionsState { + return new InlineCompletionsState([], undefined); } - private readonly _updatedEditObj: UpdatedEdit; // helper as derivedHandleChanges can not access previous value - public get updatedEdit(): IObservable { return this._updatedEditObj.offsetEdit; } - public get updatedEditModelVersion() { return this._updatedEditObj.modelVersion; } - - public get source() { return this.inlineCompletion.source; } - public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } - public get isInlineEdit() { return this.inlineCompletion.isInlineEdit; } - constructor( - public readonly inlineCompletion: InlineCompletionItem, - updatedRange: Range | undefined, - private readonly _textModel: ITextModel, - private readonly _modelVersion: IObservableWithChange, - public readonly request: UpdateRequest, + public readonly inlineCompletions: readonly InlineSuggestionItem[], + public readonly request: UpdateRequest | undefined, ) { - super(); - - this._updatedEditObj = this._register(this._toUpdatedEdit(updatedRange ?? this.inlineCompletion.range, this.inlineCompletion.insertText)); - } - - public toInlineCompletion(reader: IReader | undefined): InlineCompletionItem { - const singleTextEdit = this.toSingleTextEdit(reader); - return this.inlineCompletion.withRangeInsertTextAndFilterText(singleTextEdit.range, singleTextEdit.text, singleTextEdit.text); - } - - public toSingleTextEdit(reader: IReader | undefined): SingleTextEdit { - this._modelVersion.read(reader); - const offsetEdit = this.updatedEdit.read(reader); - if (!offsetEdit) { - return new SingleTextEdit(this._updatedRange.read(reader) ?? emptyRange, this.inlineCompletion.insertText); - } - - const startOffset = offsetEdit.edits[0].replaceRange.start; - const endOffset = offsetEdit.edits[offsetEdit.edits.length - 1].replaceRange.endExclusive; - const overallOffsetRange = new OffsetRange(startOffset, endOffset); - const overallLnColRange = Range.fromPositions( - this._textModel.getPositionAt(overallOffsetRange.start), - this._textModel.getPositionAt(overallOffsetRange.endExclusive) - ); - let text = this._textModel.getValueInRange(overallLnColRange); - for (let i = offsetEdit.edits.length - 1; i >= 0; i--) { - const edit = offsetEdit.edits[i]; - const relativeStartOffset = edit.replaceRange.start - startOffset; - const relativeEndOffset = edit.replaceRange.endExclusive - startOffset; - text = text.substring(0, relativeStartOffset) + edit.newText + text.substring(relativeEndOffset); - } - return new SingleTextEdit(overallLnColRange, text); - } - - public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { - const minimizedReplacement = singleTextRemoveCommonPrefix(this.toSingleTextEdit(reader), model); - const updatedRange = this._updatedRange.read(reader); - if ( - !updatedRange - || !this.inlineCompletion.range.getStartPosition().equals(updatedRange.getStartPosition()) - || cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber - || minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible - ) { - return false; - } - - // We might consider comparing by .toLowerText, but this requires GhostTextReplacement - const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF); - const filterText = minimizedReplacement.text; - - const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn); - - let filterTextBefore = filterText.substring(0, cursorPosIndex); - let filterTextAfter = filterText.substring(cursorPosIndex); - - let originalValueBefore = originalValue.substring(0, cursorPosIndex); - let originalValueAfter = originalValue.substring(cursorPosIndex); - - const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber); - if (minimizedReplacement.range.startColumn <= originalValueIndent) { - // Remove indentation - originalValueBefore = originalValueBefore.trimStart(); - if (originalValueBefore.length === 0) { - originalValueAfter = originalValueAfter.trimStart(); - } - filterTextBefore = filterTextBefore.trimStart(); - if (filterTextBefore.length === 0) { - filterTextAfter = filterTextAfter.trimStart(); - } - } - - return filterTextBefore.startsWith(originalValueBefore) - && !!matchesSubString(originalValueAfter, filterTextAfter); - } - - public reuse(): void { - this._updatedEditObj.reuse(); - } - - public canBeReused(model: ITextModel, position: Position): boolean { - if (!this.updatedEdit.get()) { - return false; - } - - if (this.sourceInlineCompletion.isInlineEdit) { - return this._updatedEditObj.lastChangePartOfInlineEdit; - } - - const updatedRange = this._updatedRange.read(undefined); - const result = !!updatedRange - && updatedRange.containsPosition(position) - && this.isVisible(model, position, undefined) - && TextLength.ofRange(updatedRange).isGreaterThanOrEqualTo(TextLength.ofRange(this.inlineCompletion.range)); - return result; - } - - private readonly _updatedRange = derived(reader => { - const edit = this.updatedEdit.read(reader); - if (!edit || edit.edits.length === 0) { - return undefined; - } - - return Range.fromPositions( - this._textModel.getPositionAt(edit.edits[0].replaceRange.start), - this._textModel.getPositionAt(edit.edits[edit.edits.length - 1].replaceRange.endExclusive) - ); - }); - - private _toUpdatedEdit(editRange: Range, replaceText: string): UpdatedEdit { - return this.isInlineEdit - ? this._toInlineEditEdit(editRange, replaceText) - : this._toInlineCompletionEdit(editRange, replaceText); - } - - private _toInlineCompletionEdit(editRange: Range, replaceText: string): UpdatedEdit { - const startOffset = this._textModel.getOffsetAt(editRange.getStartPosition()); - const endOffset = this._textModel.getOffsetAt(editRange.getEndPosition()); - const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); - const offsetEdit = new OffsetEdit([new SingleOffsetEdit(originalRange, replaceText)]); - return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, false); - } - - private _toInlineEditEdit(editRange: Range, replaceText: string): UpdatedEdit { - const eol = this._textModel.getEOL(); - const editOriginalText = this._textModel.getValueInRange(editRange); - const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); - - const diffAlgorithm = linesDiffComputers.getDefault(); - const lineDiffs = diffAlgorithm.computeDiff( - splitLines(editOriginalText), - splitLines(editReplaceText), - { - ignoreTrimWhitespace: false, - computeMoves: false, - extendToSubwords: true, - maxComputationTimeMs: 500, - } - ); - - const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []); - - function addRangeToPos(pos: Position, range: Range): Range { - const start = TextLength.fromPosition(range.getStartPosition()); - return TextLength.ofRange(range).createRange(start.addToPosition(pos)); - } - - const modifiedText = new StringText(editReplaceText); - - const offsetEdit = new OffsetEdit( - innerChanges.map(c => { - const range = addRangeToPos(editRange.getStartPosition(), c.originalRange); - const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); - const endOffset = this._textModel.getOffsetAt(range.getEndPosition()); - const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); - - const replaceText = modifiedText.getValueOfRange(c.modifiedRange); - const originalText = this._textModel.getValueInRange(range); - const edit = new SingleOffsetEdit(originalRange, replaceText); - - return reshapeEdit(edit, originalText, innerChanges.length, this._textModel); - }) - ); - - return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, true); - } -} - -class UpdatedEdit extends Disposable { - - private _innerEdits: SingleUpdatedEdit[]; - - private _inlineEditModelVersion: number; - public get modelVersion() { return this._inlineEditModelVersion; } - - private _lastChangePartOfInlineEdit = false; - public get lastChangePartOfInlineEdit() { return this._lastChangePartOfInlineEdit; } - - protected readonly _updatedEdit = derivedHandleChanges({ - owner: this, - equalityComparer: equalsIfDefined((a, b) => a?.equals(b)), - createEmptyChangeSummary: () => [] as OffsetEdit[], - handleChange: (context, changeSummary) => { - if (context.didChange(this._modelVersion) && context.change) { - changeSummary.push(OffsetEdits.fromContentChanges(context.change.changes)); - } - return true; - } - }, (reader, changeSummary) => { - this._modelVersion.read(reader); - - for (const change of changeSummary) { - this._innerEdits = this._applyTextModelChanges(change, this._innerEdits); - } - - if (this._innerEdits.length === 0) { - return undefined; - } - - if (this._innerEdits.some(e => e.edit === undefined)) { - throw new BugIndicatingError('UpdatedEdit: Invalid state'); + for (const inlineCompletion of inlineCompletions) { + inlineCompletion.addRef(); } - return new OffsetEdit(this._innerEdits.map(edit => edit.edit!)); - }); - - public get offsetEdit(): IObservable { return this._updatedEdit.map(e => e ?? undefined); } - - constructor( - offsetEdit: OffsetEdit, - private readonly _textModel: ITextModel, - private readonly _modelVersion: IObservableWithChange, - isInlineEdit: boolean, - ) { super(); - this._inlineEditModelVersion = this._modelVersion.get() ?? -1; - - this._innerEdits = offsetEdit.edits.map(edit => { - if (isInlineEdit) { - const replacedRange = Range.fromPositions(this._textModel.getPositionAt(edit.replaceRange.start), this._textModel.getPositionAt(edit.replaceRange.endExclusive)); - const replacedText = this._textModel.getValueInRange(replacedRange); - return new SingleUpdatedNextEdit(edit, replacedText); + this._register({ + dispose: () => { + for (const inlineCompletion of this.inlineCompletions) { + inlineCompletion.removeRef(); + } } - - return new SingleUpdatedCompletion(edit); }); - - this._updatedEdit.recomputeInitiallyAndOnChange(this._store); // make sure to call this after setting `_lastEdit` - } - - private _applyTextModelChanges(textModelChanges: OffsetEdit, edits: SingleUpdatedEdit[]): SingleUpdatedEdit[] { - for (const innerEdit of edits) { - innerEdit.applyTextModelChanges(textModelChanges); - } - - if (edits.some(edit => edit.edit === undefined)) { - return []; // change is invalid, so we will have to drop the completion - } - - const currentModelVersion = this._modelVersion.get(); - - this._lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit); - if (this._lastChangePartOfInlineEdit) { - this._inlineEditModelVersion = currentModelVersion ?? -1; - } - - if (currentModelVersion === null || this._inlineEditModelVersion + 20 < currentModelVersion) { - return []; // the completion has been ignored for a while, remove it - } - - edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty); - if (edits.length === 0) { - return []; // the completion has been typed by the user - } - - return edits; } - reuse(): void { - this._inlineEditModelVersion = this._modelVersion.get() ?? -1; + private _findById(id: InlineSuggestionIdentity): InlineSuggestionItem | undefined { + return this.inlineCompletions.find(i => i.identity === id); } -} - -abstract class SingleUpdatedEdit { - private _edit: SingleOffsetEdit | undefined; - public get edit() { return this._edit; } - - private _lastChangeUpdatedEdit = false; - public get lastChangeUpdatedEdit() { return this._lastChangeUpdatedEdit; } - - constructor( - edit: SingleOffsetEdit, - ) { - this._edit = edit; + private _findByHash(hash: string): InlineSuggestionItem | undefined { + return this.inlineCompletions.find(i => i.hash === hash); } - public applyTextModelChanges(textModelChanges: OffsetEdit) { - this._lastChangeUpdatedEdit = false; - - if (!this._edit) { - throw new BugIndicatingError('UpdatedInnerEdits: No edit to apply changes to'); - } - - const result = this.applyChanges(this._edit, textModelChanges); - if (!result) { - this._edit = undefined; - return; - } - - this._edit = result.edit; - this._lastChangeUpdatedEdit = result.editHasChanged; + /** + * Applies the edit on the state. + */ + public createStateWithAppliedEdit(edit: OffsetEdit, textModel: ITextModel): InlineCompletionsState { + const newInlineCompletions = this.inlineCompletions.map(i => i.withEdit(edit, textModel)).filter(isDefined); + return new InlineCompletionsState(newInlineCompletions, this.request); } - protected abstract applyChanges(edit: SingleOffsetEdit, textModelChanges: OffsetEdit): { edit: SingleOffsetEdit; editHasChanged: boolean } | undefined; -} - -class SingleUpdatedCompletion extends SingleUpdatedEdit { + public createStateWithAppliedResults(update: InlineCompletionProviderResult, request: UpdateRequest, textModel: ITextModel, itemToPreserve: InlineSuggestionIdentity | undefined): InlineCompletionsState { + const items: InlineSuggestionItem[] = []; - constructor( - edit: SingleOffsetEdit, - ) { - super(edit); - } - - protected applyChanges(edit: SingleOffsetEdit, textModelChanges: OffsetEdit): { edit: SingleOffsetEdit; editHasChanged: boolean } { - const newEditRange = applyEditsToRanges([edit.replaceRange], textModelChanges)[0]; - return { edit: new SingleOffsetEdit(newEditRange, edit.newText), editHasChanged: !newEditRange.equals(edit.replaceRange) }; - } -} - -class SingleUpdatedNextEdit extends SingleUpdatedEdit { - - private _trimmedNewText: string; - private _prefixLength: number; - private _suffixLength: number; - - constructor( - edit: SingleOffsetEdit, - replacedText: string, - ) { - super(edit); - - this._prefixLength = commonPrefixLength(edit.newText, replacedText); - this._suffixLength = commonSuffixLength(edit.newText, replacedText); - this._trimmedNewText = edit.newText.substring(this._prefixLength, edit.newText.length - this._suffixLength); - } - - protected applyChanges(edit: SingleOffsetEdit, textModelChanges: OffsetEdit): { edit: SingleOffsetEdit; editHasChanged: boolean } | undefined { - let editStart = edit.replaceRange.start; - let editEnd = edit.replaceRange.endExclusive; - let editReplaceText = edit.newText; - let editHasChanged = false; - - const shouldPreserveEditShape = this._prefixLength > 0 || this._suffixLength > 0; - - for (let i = textModelChanges.edits.length - 1; i >= 0; i--) { - const change = textModelChanges.edits[i]; - - // INSERTIONS (only support inserting at start of edit) - const isInsertion = change.newText.length > 0 && change.replaceRange.isEmpty; - - if (isInsertion && !shouldPreserveEditShape && change.replaceRange.start === editStart && editReplaceText.startsWith(change.newText)) { - editStart += change.newText.length; - editReplaceText = editReplaceText.substring(change.newText.length); - editEnd = Math.max(editStart, editEnd); - editHasChanged = true; - continue; - } - - if (isInsertion && shouldPreserveEditShape && change.replaceRange.start === editStart + this._prefixLength && this._trimmedNewText.startsWith(change.newText)) { - editEnd += change.newText.length; - editHasChanged = true; - this._prefixLength += change.newText.length; - this._trimmedNewText = this._trimmedNewText.substring(change.newText.length); - continue; - } - - // DELETIONS - const isDeletion = change.newText.length === 0 && change.replaceRange.length > 0; - if (isDeletion && change.replaceRange.start >= editStart + this._prefixLength && change.replaceRange.endExclusive <= editEnd - this._suffixLength) { - // user deleted text IN-BETWEEN the deletion range - editEnd -= change.replaceRange.length; - editHasChanged = true; - continue; + for (const item of update.completions) { + const i = InlineSuggestionItem.create(item, textModel); + const oldItem = this._findByHash(i.hash); + if (oldItem) { + items.push(i.withIdentity(oldItem.identity)); + oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() }); + } else { + items.push(i); } - - // user did exactly the edit - if (change.equals(edit)) { - editHasChanged = true; - editStart = change.replaceRange.endExclusive; - editReplaceText = ''; - continue; - } - - // MOVE EDIT - if (change.replaceRange.start > editEnd) { - // the change happens after the completion range - continue; - } - if (change.replaceRange.endExclusive < editStart) { - // the change happens before the completion range - editStart += change.newText.length - change.replaceRange.length; - editEnd += change.newText.length - change.replaceRange.length; - continue; - } - - // The change intersects the completion, so we will have to drop the completion - return undefined; - } - - // the resulting edit is a noop as the original and new text are the same - if (this._trimmedNewText.length === 0 && editStart + this._prefixLength === editEnd - this._suffixLength) { - return { edit: new SingleOffsetEdit(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true }; - } - - return { edit: new SingleOffsetEdit(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged }; - } -} - -const emptyRange = new Range(1, 1, 1, 1); - -function reshapeEdit(edit: SingleOffsetEdit, originalText: string, totalInnerEdits: number, textModel: ITextModel): SingleOffsetEdit { - // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 - const eol = textModel.getEOL(); - if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) { - edit = new SingleOffsetEdit(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length)); - } - - // INSERTION - // If the insertion ends with a new line and is inserted at the start of a line which has text, - // we move the insertion to the end of the previous line if possible - if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) { - edit = reshapeMultiLineInsertion(edit, textModel); - } - - // The diff algorithm extended a simple edit to the entire word - // shrink it back to a simple edit if it is deletion/insertion only - if (totalInnerEdits === 1) { - const prefixLength = commonPrefixLength(originalText, edit.newText); - const suffixLength = commonSuffixLength(originalText.slice(prefixLength), edit.newText.slice(prefixLength)); - - // reshape it back to an insertion - if (prefixLength + suffixLength === originalText.length) { - return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength)); } - // reshape it back to a deletion - if (prefixLength + suffixLength === edit.newText.length) { - return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), ''); + if (itemToPreserve) { + const item = this._findById(itemToPreserve); + if (item && !update.has(item.getSingleTextEdit()) && item.canBeReused(textModel, request.position)) { + items.unshift(item); + } } - } - return edit; -} - -function reshapeMultiLineInsertion(edit: SingleOffsetEdit, textModel: ITextModel): SingleOffsetEdit { - if (!edit.replaceRange.isEmpty) { - throw new BugIndicatingError('Unexpected original range'); - } - - if (edit.replaceRange.start === 0) { - return edit; + return new InlineCompletionsState(items, request); } - const eol = textModel.getEOL(); - const startPosition = textModel.getPositionAt(edit.replaceRange.start); - const startColumn = startPosition.column; - const startLineNumber = startPosition.lineNumber; - - // If the insertion ends with a new line and is inserted at the start of a line which has text, - // we move the insertion to the end of the previous line if possible - if (startColumn === 1 && startLineNumber > 1 && textModel.getLineLength(startLineNumber) !== 0 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) { - return new SingleOffsetEdit(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length)); + public clone(): InlineCompletionsState { + return new InlineCompletionsState(this.inlineCompletions, this.request); } - - return edit; } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts index 13e6d6edf74..f7ba383b8f4 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts @@ -5,13 +5,13 @@ import { SingleTextEdit } from '../../../../common/core/textEdit.js'; import { Command } from '../../../../common/languages.js'; -import { InlineCompletionItem } from './provideInlineCompletions.js'; +import { InlineSuggestionItem } from './inlineSuggestionItem.js'; export class InlineEdit { constructor( public readonly edit: SingleTextEdit, public readonly commands: readonly Command[], - public readonly inlineCompletion: InlineCompletionItem, + public readonly inlineCompletion: InlineSuggestionItem, ) { } public get range() { diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts new file mode 100644 index 00000000000..e930859c2e7 --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -0,0 +1,646 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { matchesSubString } from '../../../../../base/common/filters.js'; +import { observableSignal, IObservable } from '../../../../../base/common/observable.js'; +import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; +import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; +import { OffsetRange } from '../../../../common/core/offsetRange.js'; +import { Position } from '../../../../common/core/position.js'; +import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformerBase } from '../../../../common/core/positionToOffset.js'; +import { Range } from '../../../../common/core/range.js'; +import { SingleTextEdit, StringText, TextEdit } from '../../../../common/core/textEdit.js'; +import { TextLength } from '../../../../common/core/textLength.js'; +import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; +import { InlineCompletion, InlineCompletionTriggerKind, Command, InlineCompletionWarning, PartialAcceptInfo, InlineCompletionEndOfLifeReason } from '../../../../common/languages.js'; +import { ITextModel, EndOfLinePreference } from '../../../../common/model.js'; +import { TextModelText } from '../../../../common/model/textModelText.js'; +import { IDisplayLocation, InlineSuggestData, InlineSuggestionList, SnippetInfo } from './provideInlineCompletions.js'; +import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; + +export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem; + +export namespace InlineSuggestionItem { + export function create( + data: InlineSuggestData, + textModel: ITextModel, + ): InlineSuggestionItem { + if (!data.isInlineEdit) { + return InlineCompletionItem.create(data, textModel); + } else { + return InlineEditItem.create(data, textModel); + } + } +} + +abstract class InlineSuggestionItemBase { + constructor( + protected readonly _data: InlineSuggestData, + public readonly identity: InlineSuggestionIdentity, + public readonly displayLocation: InlineSuggestDisplayLocation | undefined + ) { } + + /** + * A reference to the original inline completion list this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ + public get source(): InlineSuggestionList { return this._data.source; } + + public get isFromExplicitRequest(): boolean { return this._data.context.triggerKind === InlineCompletionTriggerKind.Explicit; } + public get forwardStable(): boolean { return this.source.inlineSuggestions.enableForwardStability ?? false; } + public get editRange(): Range { return this.getSingleTextEdit().range; } + public get targetRange(): Range { return this.displayLocation?.range ?? this.editRange; } + public get insertText(): string { return this.getSingleTextEdit().text; } + public get semanticId(): string { return this.hash; } + public get action(): Command | undefined { return this._sourceInlineCompletion.action; } + public get command(): Command | undefined { return this._sourceInlineCompletion.command; } + public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } + public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } + public get hash() { + return JSON.stringify([ + this.getSingleTextEdit().text, + this.getSingleTextEdit().range.getStartPosition().toString() + ]); + } + /** @deprecated */ + public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; } + + + /** + * A reference to the original inline completion this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ + private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; } + + + public abstract getSingleTextEdit(): SingleTextEdit; + + public abstract withEdit(userEdit: OffsetEdit, textModel: ITextModel): InlineSuggestionItem | undefined; + + public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem; + public abstract canBeReused(model: ITextModel, position: Position): boolean; + + + public addRef(): void { + this.identity.addRef(); + this.source.addRef(); + } + + public removeRef(): void { + this.identity.removeRef(); + this.source.removeRef(); + } + + public reportInlineEditShown(commandService: ICommandService) { + this._data.reportInlineEditShown(commandService, this.insertText); + } + + public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo) { + this._data.reportPartialAccept(acceptedCharacters, info); + } + + public reportEndOfLife(reason: InlineCompletionEndOfLifeReason): void { + this._data.reportEndOfLife(reason); + } + + public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void { + this._data.setEndOfLifeReason(reason); + } + + /** + * Avoid using this method. Instead introduce getters for the needed properties. + */ + public getSourceCompletion(): InlineCompletion { + return this._sourceInlineCompletion; + } +} + +export class InlineSuggestionIdentity { + private static idCounter = 0; + private readonly _onDispose = observableSignal(this); + public readonly onDispose: IObservable = this._onDispose; + + private _refCount = 1; + public readonly id = 'InlineCompletionIdentity' + InlineSuggestionIdentity.idCounter++; + + addRef() { + this._refCount++; + } + + removeRef() { + this._refCount--; + if (this._refCount === 0) { + this._onDispose.trigger(undefined); + } + } +} + +class InlineSuggestDisplayLocation implements IDisplayLocation { + + public static create(displayLocation: IDisplayLocation, textmodel: ITextModel) { + const offsetRange = new OffsetRange( + textmodel.getOffsetAt(displayLocation.range.getStartPosition()), + textmodel.getOffsetAt(displayLocation.range.getEndPosition()) + ); + + return new InlineSuggestDisplayLocation( + offsetRange, + displayLocation.range, + displayLocation.label, + ); + } + + private constructor( + private readonly _offsetRange: OffsetRange, + public readonly range: Range, + public readonly label: string, + ) { } + + public withEdit(edit: OffsetEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestDisplayLocation | undefined { + const newOffsetRange = applyEditsToRanges([this._offsetRange], edit)[0]; + if (!newOffsetRange || newOffsetRange.length !== this._offsetRange.length) { + return undefined; + } + + const newRange = positionOffsetTransformer.getRange(newOffsetRange); + + return new InlineSuggestDisplayLocation( + newOffsetRange, + newRange, + this.label, + ); + } +} + +export class InlineCompletionItem extends InlineSuggestionItemBase { + public static create( + data: InlineSuggestData, + textModel: ITextModel, + ): InlineCompletionItem { + const identity = new InlineSuggestionIdentity(); + const textEdit = new SingleTextEdit(data.range, data.insertText); + const edit = getPositionOffsetTransformerFromTextModel(textModel).getSingleOffsetEdit(textEdit); + const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; + + return new InlineCompletionItem(edit, textEdit, data.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); + } + + public readonly isInlineEdit = false; + + private constructor( + private readonly _edit: SingleOffsetEdit, + private readonly _textEdit: SingleTextEdit, + private readonly _originalRange: Range, + public readonly snippetInfo: SnippetInfo | undefined, + public readonly additionalTextEdits: readonly ISingleEditOperation[], + + data: InlineSuggestData, + identity: InlineSuggestionIdentity, + displayLocation: InlineSuggestDisplayLocation | undefined, + ) { + super(data, identity, displayLocation); + } + + override getSingleTextEdit(): SingleTextEdit { return this._textEdit; } + + override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem { + return new InlineCompletionItem( + this._edit, + this._textEdit, + this._originalRange, + this.snippetInfo, + this.additionalTextEdits, + this._data, + identity, + this.displayLocation + ); + } + + override withEdit(textModelEdit: OffsetEdit, textModel: ITextModel): InlineCompletionItem | undefined { + const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit); + if (newEditRange.length === 0) { + return undefined; + } + const newEdit = new SingleOffsetEdit(newEditRange[0], this._textEdit.text); + const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); + const newTextEdit = positionOffsetTransformer.getSingleTextEdit(newEdit); + + let newDisplayLocation = this.displayLocation; + if (newDisplayLocation) { + newDisplayLocation = newDisplayLocation.withEdit(textModelEdit, positionOffsetTransformer); + if (!newDisplayLocation) { + return undefined; + } + } + + return new InlineCompletionItem( + newEdit, + newTextEdit, + this._originalRange, + this.snippetInfo, + this.additionalTextEdits, + this._data, + this.identity, + newDisplayLocation + ); + } + + override canBeReused(model: ITextModel, position: Position): boolean { + // TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion. + const updatedRange = this._textEdit.range; + const result = !!updatedRange + && updatedRange.containsPosition(position) + && this.isVisible(model, position) + && TextLength.ofRange(updatedRange).isGreaterThanOrEqualTo(TextLength.ofRange(this._originalRange)); + return result; + } + + public isVisible(model: ITextModel, cursorPosition: Position): boolean { + const minimizedReplacement = singleTextRemoveCommonPrefix(this.getSingleTextEdit(), model); + if (!this.editRange + || !this._originalRange.getStartPosition().equals(this.editRange.getStartPosition()) + || cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber + || minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible + ) { + return false; + } + + // We might consider comparing by .toLowerText, but this requires GhostTextReplacement + const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF); + const filterText = minimizedReplacement.text; + + const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn); + + let filterTextBefore = filterText.substring(0, cursorPosIndex); + let filterTextAfter = filterText.substring(cursorPosIndex); + + let originalValueBefore = originalValue.substring(0, cursorPosIndex); + let originalValueAfter = originalValue.substring(cursorPosIndex); + + const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber); + if (minimizedReplacement.range.startColumn <= originalValueIndent) { + // Remove indentation + originalValueBefore = originalValueBefore.trimStart(); + if (originalValueBefore.length === 0) { + originalValueAfter = originalValueAfter.trimStart(); + } + filterTextBefore = filterTextBefore.trimStart(); + if (filterTextBefore.length === 0) { + filterTextAfter = filterTextAfter.trimStart(); + } + } + + return filterTextBefore.startsWith(originalValueBefore) + && !!matchesSubString(originalValueAfter, filterTextAfter); + } +} + +export class InlineEditItem extends InlineSuggestionItemBase { + public static create( + data: InlineSuggestData, + textModel: ITextModel, + ): InlineEditItem { + const offsetEdit = getOffsetEdit(textModel, data.range, data.insertText); + const text = new TextModelText(textModel); + const textEdit = TextEdit.fromOffsetEdit(offsetEdit, text); + const singleTextEdit = textEdit.toSingle(text); + const identity = new InlineSuggestionIdentity(); + + const edits = offsetEdit.edits.map(edit => { + const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getPositionAt(edit.replaceRange.endExclusive)); + const replacedText = textModel.getValueInRange(replacedRange); + return SingleUpdatedNextEdit.create(edit, replacedText); + }); + const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; + return new InlineEditItem(offsetEdit, singleTextEdit, data, identity, edits, displayLocation, false, textModel.getVersionId()); + } + + public readonly snippetInfo: SnippetInfo | undefined = undefined; + public readonly additionalTextEdits: readonly ISingleEditOperation[] = []; + public readonly isInlineEdit = true; + + private constructor( + private readonly _edit: OffsetEdit, + private readonly _textEdit: SingleTextEdit, + + data: InlineSuggestData, + + identity: InlineSuggestionIdentity, + private readonly _edits: readonly SingleUpdatedNextEdit[], + displayLocation: InlineSuggestDisplayLocation | undefined, + private readonly _lastChangePartOfInlineEdit = false, + private readonly _inlineEditModelVersion: number, + ) { + super(data, identity, displayLocation); + } + + public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; } + public get updatedEdit(): OffsetEdit { return this._edit; } + + override getSingleTextEdit(): SingleTextEdit { + return this._textEdit; + } + + override withIdentity(identity: InlineSuggestionIdentity): InlineEditItem { + return new InlineEditItem( + this._edit, + this._textEdit, + this._data, + identity, + this._edits, + this.displayLocation, + this._lastChangePartOfInlineEdit, + this._inlineEditModelVersion, + ); + } + + override canBeReused(model: ITextModel, position: Position): boolean { + // TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion. + return this._lastChangePartOfInlineEdit && this.updatedEditModelVersion === model.getVersionId(); + } + + override withEdit(textModelChanges: OffsetEdit, textModel: ITextModel): InlineEditItem | undefined { + const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel); + return edit; + } + + private _applyTextModelChanges(textModelChanges: OffsetEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined { + edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges)); + + if (edits.some(edit => edit.edit === undefined)) { + return undefined; // change is invalid, so we will have to drop the completion + } + + const newTextModelVersion = textModel.getVersionId(); + + let inlineEditModelVersion = this._inlineEditModelVersion; + const lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit); + if (lastChangePartOfInlineEdit) { + inlineEditModelVersion = newTextModelVersion ?? -1; + } + + if (newTextModelVersion === null || inlineEditModelVersion + 20 < newTextModelVersion) { + return undefined; // the completion has been ignored for a while, remove it + } + + edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty); + if (edits.length === 0) { + return undefined; // the completion has been typed by the user + } + + const newEdit = new OffsetEdit(edits.map(edit => edit.edit!)); + const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); + const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toSingle(new TextModelText(textModel)); + + let newDisplayLocation = this.displayLocation; + if (newDisplayLocation) { + newDisplayLocation = newDisplayLocation.withEdit(textModelChanges, positionOffsetTransformer); + if (!newDisplayLocation) { + return undefined; + } + } + + return new InlineEditItem( + newEdit, + newTextEdit, + this._data, + this.identity, + edits, + newDisplayLocation, + lastChangePartOfInlineEdit, + inlineEditModelVersion, + ); + } +} + +function getOffsetEdit(textModel: ITextModel, editRange: Range, replaceText: string): OffsetEdit { + const eol = textModel.getEOL(); + const editOriginalText = textModel.getValueInRange(editRange); + const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); + + const diffAlgorithm = linesDiffComputers.getDefault(); + const lineDiffs = diffAlgorithm.computeDiff( + splitLines(editOriginalText), + splitLines(editReplaceText), + { + ignoreTrimWhitespace: false, + computeMoves: false, + extendToSubwords: true, + maxComputationTimeMs: 500, + } + ); + + const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []); + + function addRangeToPos(pos: Position, range: Range): Range { + const start = TextLength.fromPosition(range.getStartPosition()); + return TextLength.ofRange(range).createRange(start.addToPosition(pos)); + } + + const modifiedText = new StringText(editReplaceText); + + const offsetEdit = new OffsetEdit( + innerChanges.map(c => { + const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange); + const originalRange = getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(rangeInModel); + + const replaceText = modifiedText.getValueOfRange(c.modifiedRange); + const edit = new SingleOffsetEdit(originalRange, replaceText); + + const originalText = textModel.getValueInRange(rangeInModel); + return reshapeEdit(edit, originalText, innerChanges.length, textModel); + }) + ); + + return offsetEdit; +} + +class SingleUpdatedNextEdit { + public static create( + edit: SingleOffsetEdit, + replacedText: string, + ): SingleUpdatedNextEdit { + const prefixLength = commonPrefixLength(edit.newText, replacedText); + const suffixLength = commonSuffixLength(edit.newText, replacedText); + const trimmedNewText = edit.newText.substring(prefixLength, edit.newText.length - suffixLength); + return new SingleUpdatedNextEdit(edit, trimmedNewText, prefixLength, suffixLength); + } + + public get edit() { return this._edit; } + public get lastChangeUpdatedEdit() { return this._lastChangeUpdatedEdit; } + + constructor( + private _edit: SingleOffsetEdit | undefined, + private _trimmedNewText: string, + private _prefixLength: number, + private _suffixLength: number, + private _lastChangeUpdatedEdit: boolean = false, + ) { + } + + public applyTextModelChanges(textModelChanges: OffsetEdit) { + const c = this._clone(); + c._applyTextModelChanges(textModelChanges); + return c; + } + + private _clone(): SingleUpdatedNextEdit { + return new SingleUpdatedNextEdit( + this._edit, + this._trimmedNewText, + this._prefixLength, + this._suffixLength, + this._lastChangeUpdatedEdit, + ); + } + + private _applyTextModelChanges(textModelChanges: OffsetEdit) { + this._lastChangeUpdatedEdit = false; + + if (!this._edit) { + throw new BugIndicatingError('UpdatedInnerEdits: No edit to apply changes to'); + } + + const result = this._applyChanges(this._edit, textModelChanges); + if (!result) { + this._edit = undefined; + return; + } + + this._edit = result.edit; + this._lastChangeUpdatedEdit = result.editHasChanged; + } + + private _applyChanges(edit: SingleOffsetEdit, textModelChanges: OffsetEdit): { edit: SingleOffsetEdit; editHasChanged: boolean } | undefined { + let editStart = edit.replaceRange.start; + let editEnd = edit.replaceRange.endExclusive; + let editReplaceText = edit.newText; + let editHasChanged = false; + + const shouldPreserveEditShape = this._prefixLength > 0 || this._suffixLength > 0; + + for (let i = textModelChanges.edits.length - 1; i >= 0; i--) { + const change = textModelChanges.edits[i]; + + // INSERTIONS (only support inserting at start of edit) + const isInsertion = change.newText.length > 0 && change.replaceRange.isEmpty; + + if (isInsertion && !shouldPreserveEditShape && change.replaceRange.start === editStart && editReplaceText.startsWith(change.newText)) { + editStart += change.newText.length; + editReplaceText = editReplaceText.substring(change.newText.length); + editEnd = Math.max(editStart, editEnd); + editHasChanged = true; + continue; + } + + if (isInsertion && shouldPreserveEditShape && change.replaceRange.start === editStart + this._prefixLength && this._trimmedNewText.startsWith(change.newText)) { + editEnd += change.newText.length; + editHasChanged = true; + this._prefixLength += change.newText.length; + this._trimmedNewText = this._trimmedNewText.substring(change.newText.length); + continue; + } + + // DELETIONS + const isDeletion = change.newText.length === 0 && change.replaceRange.length > 0; + if (isDeletion && change.replaceRange.start >= editStart + this._prefixLength && change.replaceRange.endExclusive <= editEnd - this._suffixLength) { + // user deleted text IN-BETWEEN the deletion range + editEnd -= change.replaceRange.length; + editHasChanged = true; + continue; + } + + // user did exactly the edit + if (change.equals(edit)) { + editHasChanged = true; + editStart = change.replaceRange.endExclusive; + editReplaceText = ''; + continue; + } + + // MOVE EDIT + if (change.replaceRange.start > editEnd) { + // the change happens after the completion range + continue; + } + if (change.replaceRange.endExclusive < editStart) { + // the change happens before the completion range + editStart += change.newText.length - change.replaceRange.length; + editEnd += change.newText.length - change.replaceRange.length; + continue; + } + + // The change intersects the completion, so we will have to drop the completion + return undefined; + } + + // the resulting edit is a noop as the original and new text are the same + if (this._trimmedNewText.length === 0 && editStart + this._prefixLength === editEnd - this._suffixLength) { + return { edit: new SingleOffsetEdit(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true }; + } + + return { edit: new SingleOffsetEdit(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged }; + } +} + +function reshapeEdit(edit: SingleOffsetEdit, originalText: string, totalInnerEdits: number, textModel: ITextModel): SingleOffsetEdit { + // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 + const eol = textModel.getEOL(); + if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) { + edit = new SingleOffsetEdit(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length)); + } + + // INSERTION + // If the insertion ends with a new line and is inserted at the start of a line which has text, + // we move the insertion to the end of the previous line if possible + if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) { + edit = reshapeMultiLineInsertion(edit, textModel); + } + + // The diff algorithm extended a simple edit to the entire word + // shrink it back to a simple edit if it is deletion/insertion only + if (totalInnerEdits === 1) { + const prefixLength = commonPrefixLength(originalText, edit.newText); + const suffixLength = commonSuffixLength(originalText.slice(prefixLength), edit.newText.slice(prefixLength)); + + // reshape it back to an insertion + if (prefixLength + suffixLength === originalText.length) { + return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength)); + } + + // reshape it back to a deletion + if (prefixLength + suffixLength === edit.newText.length) { + return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), ''); + } + } + + return edit; +} + +function reshapeMultiLineInsertion(edit: SingleOffsetEdit, textModel: ITextModel): SingleOffsetEdit { + if (!edit.replaceRange.isEmpty) { + throw new BugIndicatingError('Unexpected original range'); + } + + if (edit.replaceRange.start === 0) { + return edit; + } + + const eol = textModel.getEOL(); + const startPosition = textModel.getPositionAt(edit.replaceRange.start); + const startColumn = startPosition.column; + const startLineNumber = startPosition.lineNumber; + + // If the insertion ends with a new line and is inserted at the start of a line which has text, + // we move the insertion to the end of the previous line if possible + if (startColumn === 1 && startLineNumber > 1 && textModel.getLineLength(startLineNumber) !== 0 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) { + return new SingleOffsetEdit(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length)); + } + + return edit; +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 49c79efeafe..1cfd4b6aa91 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -10,14 +10,14 @@ import { onUnexpectedExternalError } from '../../../../../base/common/errors.js' import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { SetMap } from '../../../../../base/common/map.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit } from '../../../../common/core/textEdit.js'; -import { LanguageFeatureRegistry } from '../../../../common/languageFeatureRegistry.js'; -import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind, PartialAcceptInfo } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; @@ -26,7 +26,7 @@ import { SnippetParser, Text } from '../../../snippet/browser/snippetParser.js'; import { getReadonlyEmptyArray } from '../utils.js'; export async function provideInlineCompletions( - registry: LanguageFeatureRegistry, + providers: InlineCompletionsProvider[], positionOrRange: Position | Range, model: ITextModel, context: InlineCompletionContext, @@ -39,7 +39,6 @@ export async function provideInlineCompletions( const contextWithUuid = { ...context, requestUuid: requestUuid }; const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange; - const providers = registry.all(model); const multiMap = new SetMap>(); for (const provider of providers) { @@ -60,13 +59,12 @@ export async function provideInlineCompletions( return result; } - type Result = Promise; - const states = new Map(); + type Result = Promise; - const seen = new Set(); function findPreferredProviderCircle( provider: InlineCompletionsProvider, - stack: InlineCompletionsProvider[] + stack: InlineCompletionsProvider[], + seen: Set, ): InlineCompletionsProvider[] | undefined { stack = [...stack, provider]; if (seen.has(provider)) { return stack; } @@ -75,7 +73,7 @@ export async function provideInlineCompletions( try { const preferred = getPreferredProviders(provider); for (const p of preferred) { - const c = findPreferredProviderCircle(p, stack); + const c = findPreferredProviderCircle(p, stack, seen); if (c) { return c; } } } finally { @@ -84,25 +82,25 @@ export async function provideInlineCompletions( return undefined; } - function queryProviderOrPreferredProvider(provider: InlineCompletionsProvider): Result { + function queryProviderOrPreferredProvider(provider: InlineCompletionsProvider, states: Map): Result { const state = states.get(provider); if (state) { return state; } - const circle = findPreferredProviderCircle(provider, []); + const circle = findPreferredProviderCircle(provider, [], new Set()); if (circle) { onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected.` + ` Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`)); } - const deferredPromise = new DeferredPromise(); + const deferredPromise = new DeferredPromise(); states.set(provider, deferredPromise.p); (async () => { if (!circle) { const preferred = getPreferredProviders(provider); for (const p of preferred) { - const result = await queryProviderOrPreferredProvider(p); - if (result && result.inlineCompletions.items.length > 0) { + const result = await queryProviderOrPreferredProvider(p, states); + if (result && result.inlineSuggestions.items.length > 0) { // Skip provider return undefined; } @@ -115,7 +113,7 @@ export async function provideInlineCompletions( return deferredPromise.p; } - async function query(provider: InlineCompletionsProvider): Promise { + async function query(provider: InlineCompletionsProvider): Promise { let result: InlineCompletions | null | undefined; try { if (positionOrRange instanceof Position) { @@ -129,13 +127,18 @@ export async function provideInlineCompletions( } if (!result) { return undefined; } - const list = new InlineCompletionList(result, provider); + const data: InlineSuggestData[] = []; + const list = new InlineSuggestionList(result, data, provider); + for (const item of result.items) { + data.push(createInlineCompletionItem(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid)); + } runWhenCancelled(token, () => list.removeRef()); return list; } - const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(queryProviderOrPreferredProvider)); + const states = new Map(); + const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(p => queryProviderOrPreferredProvider(p, states))); if (token.isCancellationRequested) { tokenSource.dispose(true); @@ -143,8 +146,8 @@ export async function provideInlineCompletions( return new InlineCompletionProviderResult([], new Set(), []); } - const result = await addRefAndCreateResult(contextWithUuid, inlineCompletionLists, defaultReplaceRange, model, languageConfigurationService); - tokenSource.dispose(true); // This disposes results that are not referenced. + const result = await addRefAndCreateResult(contextWithUuid, inlineCompletionLists, model); + tokenSource.dispose(true); // This disposes results that are not referenced by now. return result; } @@ -162,43 +165,33 @@ function runWhenCancelled(token: CancellationToken, callback: () => void): IDisp } } -// TODO: check cancellation token! async function addRefAndCreateResult( context: InlineCompletionContext, - inlineCompletionLists: AsyncIterable<(InlineCompletionList | undefined)>, - defaultReplaceRange: Range, + inlineCompletionLists: AsyncIterable<(InlineSuggestionList | undefined)>, model: ITextModel, - languageConfigurationService: ILanguageConfigurationService | undefined ): Promise { // for deduplication - const itemsByHash = new Map(); + const itemsByHash = new Map(); let shouldStop = false; - const lists: InlineCompletionList[] = []; + const lists: InlineSuggestionList[] = []; for await (const completions of inlineCompletionLists) { if (!completions) { continue; } completions.addRef(); lists.push(completions); - for (const item of completions.inlineCompletions.items) { + for (const item of completions.inlineSuggestionsData) { if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) { continue; } if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) { continue; } - const inlineCompletionItem = InlineCompletionItem.from( - item, - completions, - defaultReplaceRange, - model, - languageConfigurationService - ); - itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem); + itemsByHash.set(createHashFromSingleTextEdit(item.getSingleTextEdit()), item); // Stop after first visible inline completion if (!(item.isInlineEdit || item.showInlineEditMenu) && context.triggerKind === InlineCompletionTriggerKind.Automatic) { - const minifiedEdit = inlineCompletionItem.toSingleTextEdit().removeCommonPrefix(new TextModelText(model)); + const minifiedEdit = item.getSingleTextEdit().removeCommonPrefix(new TextModelText(model)); if (!minifiedEdit.isEmpty) { shouldStop = true; } @@ -219,13 +212,13 @@ export class InlineCompletionProviderResult implements IDisposable { /** * Free of duplicates. */ - public readonly completions: readonly InlineCompletionItem[], + public readonly completions: readonly InlineSuggestData[], private readonly hashs: Set, - private readonly providerResults: readonly InlineCompletionList[], + private readonly providerResults: readonly InlineSuggestionList[], ) { } - public has(item: InlineCompletionItem): boolean { - return this.hashs.has(item.hash()); + public has(edit: SingleTextEdit): boolean { + return this.hashs.has(createHashFromSingleTextEdit(edit)); } // TODO: This is not complete as it does not take the textmodel into account @@ -241,177 +234,168 @@ export class InlineCompletionProviderResult implements IDisposable { } } -/** - * A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that - * computed them. - */ -export class InlineCompletionList { - private refCount = 1; - constructor( - public readonly inlineCompletions: InlineCompletions, - public readonly provider: InlineCompletionsProvider, - ) { } +function createHashFromSingleTextEdit(edit: SingleTextEdit): string { + return JSON.stringify([edit.text, edit.range.getStartPosition().toString()]); +} - addRef(): void { - this.refCount++; - } +function createInlineCompletionItem( + inlineCompletion: InlineCompletion, + source: InlineSuggestionList, + defaultReplaceRange: Range, + textModel: ITextModel, + languageConfigurationService: ILanguageConfigurationService | undefined, + context: InlineCompletionContext, +): InlineSuggestData { + let insertText: string; + let snippetInfo: SnippetInfo | undefined; + let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange; + + if (typeof inlineCompletion.insertText === 'string') { + insertText = inlineCompletion.insertText; + + if (languageConfigurationService && inlineCompletion.completeBracketPairs) { + insertText = closeBrackets( + insertText, + range.getStartPosition(), + textModel, + languageConfigurationService + ); - removeRef(): void { - this.refCount--; - if (this.refCount === 0) { - this.provider.freeInlineCompletions(this.inlineCompletions); + // Modify range depending on if brackets are added or removed + const diff = insertText.length - inlineCompletion.insertText.length; + if (diff !== 0) { + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); + } } - } -} -export class InlineCompletionItem { - public static from( - inlineCompletion: InlineCompletion, - source: InlineCompletionList, - defaultReplaceRange: Range, - textModel: ITextModel, - languageConfigurationService: ILanguageConfigurationService | undefined, - ) { - let insertText: string; - let snippetInfo: SnippetInfo | undefined; - let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange; - - if (typeof inlineCompletion.insertText === 'string') { - insertText = inlineCompletion.insertText; - - if (languageConfigurationService && inlineCompletion.completeBracketPairs) { - insertText = closeBrackets( - insertText, - range.getStartPosition(), - textModel, - languageConfigurationService - ); - - // Modify range depending on if brackets are added or removed - const diff = insertText.length - inlineCompletion.insertText.length; - if (diff !== 0) { - range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); - } - } + snippetInfo = undefined; + } else if ('snippet' in inlineCompletion.insertText) { + const preBracketCompletionLength = inlineCompletion.insertText.snippet.length; - snippetInfo = undefined; - } else if ('snippet' in inlineCompletion.insertText) { - const preBracketCompletionLength = inlineCompletion.insertText.snippet.length; - - if (languageConfigurationService && inlineCompletion.completeBracketPairs) { - inlineCompletion.insertText.snippet = closeBrackets( - inlineCompletion.insertText.snippet, - range.getStartPosition(), - textModel, - languageConfigurationService - ); - - // Modify range depending on if brackets are added or removed - const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength; - if (diff !== 0) { - range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); - } + if (languageConfigurationService && inlineCompletion.completeBracketPairs) { + inlineCompletion.insertText.snippet = closeBrackets( + inlineCompletion.insertText.snippet, + range.getStartPosition(), + textModel, + languageConfigurationService + ); + + // Modify range depending on if brackets are added or removed + const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength; + if (diff !== 0) { + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); } + } - const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet); + const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet); - if (snippet.children.length === 1 && snippet.children[0] instanceof Text) { - insertText = snippet.children[0].value; - snippetInfo = undefined; - } else { - insertText = snippet.toString(); - snippetInfo = { - snippet: inlineCompletion.insertText.snippet, - range: range - }; - } + if (snippet.children.length === 1 && snippet.children[0] instanceof Text) { + insertText = snippet.children[0].value; + snippetInfo = undefined; } else { - assertNever(inlineCompletion.insertText); + insertText = snippet.toString(); + snippetInfo = { + snippet: inlineCompletion.insertText.snippet, + range: range + }; } - - return new InlineCompletionItem( - insertText, - inlineCompletion.command, - inlineCompletion.shownCommand, - inlineCompletion.action, - range, - insertText, - snippetInfo, - Range.lift(inlineCompletion.showRange) ?? undefined, - inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), - inlineCompletion, - source, - ); + } else { + assertNever(inlineCompletion.insertText); } - static ID = 1; + const displayLocation = inlineCompletion.displayLocation ? { + range: Range.lift(inlineCompletion.displayLocation.range), + label: inlineCompletion.displayLocation.label + } : undefined; + + return new InlineSuggestData( + range, + insertText, + snippetInfo, + displayLocation, + inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), + inlineCompletion, + source, + context, + inlineCompletion.isInlineEdit ?? false, + ); +} - private _didCallShow = false; +export class InlineSuggestData { + private _didShow = false; + private _didReportEndOfLife = false; + private _lastSetEndOfLifeReason: InlineCompletionEndOfLifeReason | undefined = undefined; constructor( - readonly filterText: string, - readonly command: Command | undefined, - /** @deprecated. Use handleItemDidShow */ - readonly shownCommand: Command | undefined, - readonly action: Command | undefined, - readonly range: Range, - readonly insertText: string, - readonly snippetInfo: SnippetInfo | undefined, - readonly cursorShowRange: Range | undefined, - - readonly additionalTextEdits: readonly ISingleEditOperation[], - - - /** - * A reference to the original inline completion this inline completion has been constructed from. - * Used for event data to ensure referential equality. - */ - readonly sourceInlineCompletion: InlineCompletion, + public readonly range: Range, + public readonly insertText: string, + public readonly snippetInfo: SnippetInfo | undefined, + public readonly displayLocation: IDisplayLocation | undefined, + public readonly additionalTextEdits: readonly ISingleEditOperation[], + + public readonly sourceInlineCompletion: InlineCompletion, + public readonly source: InlineSuggestionList, + public readonly context: InlineCompletionContext, + public readonly isInlineEdit: boolean, + ) { } - /** - * A reference to the original inline completion list this inline completion has been constructed from. - * Used for event data to ensure referential equality. - */ - readonly source: InlineCompletionList, + public get showInlineEditMenu() { return this.sourceInlineCompletion.showInlineEditMenu ?? false; } - readonly id = `InlineCompletion:${InlineCompletionItem.ID++}`, - ) { + public getSingleTextEdit() { + return new SingleTextEdit(this.range, this.insertText); } - get isInlineEdit(): boolean { - return this.sourceInlineCompletion.isInlineEdit!!; - } + public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string): Promise { + if (this._didShow) { + return; + } + this._didShow = true; - public get didShow(): boolean { - return this._didCallShow; - } - public markAsShown(): void { - this._didCallShow = true; + this.source.provider.handleItemDidShow?.(this.source.inlineSuggestions, this.sourceInlineCompletion, updatedInsertText); + + if (this.sourceInlineCompletion.shownCommand) { + await commandService.executeCommand(this.sourceInlineCompletion.shownCommand.id, ...(this.sourceInlineCompletion.shownCommand.arguments || [])); + } } - public withRangeInsertTextAndFilterText(updatedRange: Range, updatedInsertText: string, updatedFilterText: string): InlineCompletionItem { - return new InlineCompletionItem( - updatedFilterText, - this.command, - this.shownCommand, - this.action, - updatedRange, - updatedInsertText, - this.snippetInfo, - this.cursorShowRange, - this.additionalTextEdits, + public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo) { + this.source.provider.handlePartialAccept?.( + this.source.inlineSuggestions, this.sourceInlineCompletion, - this.source, - this.id, + acceptedCharacters, + info ); } - public hash(): string { - return JSON.stringify({ insertText: this.insertText, range: this.range.toString() }); + /** + * Sends the end of life event to the provider. + * If no reason is provided, the last set reason is used. + * If no reason was set, the default reason is used. + */ + public reportEndOfLife(reason?: InlineCompletionEndOfLifeReason): void { + if (this._didReportEndOfLife) { + return; + } + this._didReportEndOfLife = true; + + if (!reason) { + reason = this._lastSetEndOfLifeReason ?? { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; + } + + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && this.source.provider.handleRejection) { + this.source.provider.handleRejection(this.source.inlineSuggestions, this.sourceInlineCompletion); + } + + if (this.source.provider.handleEndOfLifetime) { + this.source.provider.handleEndOfLifetime(this.source.inlineSuggestions, this.sourceInlineCompletion, reason); + } } - public toSingleTextEdit(): SingleTextEdit { - return new SingleTextEdit(this.range, this.insertText); + /** + * Sets the end of life reason, but does not send the event to the provider yet. + */ + public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void { + this._lastSetEndOfLifeReason = reason; } } @@ -421,6 +405,39 @@ export interface SnippetInfo { range: Range; } +export interface IDisplayLocation { + range: Range; + label: string; +} + +/** + * A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that + * computed them. + */ +export class InlineSuggestionList { + private refCount = 1; + constructor( + public readonly inlineSuggestions: InlineCompletions, + public readonly inlineSuggestionsData: readonly InlineSuggestData[], + public readonly provider: InlineCompletionsProvider, + ) { } + + addRef(): void { + this.refCount++; + } + + removeRef(): void { + this.refCount--; + if (this.refCount === 0) { + for (const item of this.inlineSuggestionsData) { + // Fallback if it has not been called before + item.reportEndOfLife(); + } + this.provider.freeInlineCompletions(this.inlineSuggestions); + } + } +} + function getDefaultRange(position: Position, model: ITextModel): Range { const word = model.getWordAtPosition(position); const maxColumn = model.getLineMaxColumn(position.lineNumber); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts index 88df0a98a52..bb20091f146 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts @@ -74,7 +74,7 @@ export class SuggestWidgetAdaptor extends Disposable { const candidates = suggestItems .map((suggestItem, index) => { const suggestItemInfo = SuggestItemInfo.fromSuggestion(suggestController, textModel, position, suggestItem, this.isShiftKeyPressed); - const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.toSingleTextEdit(), textModel); + const suggestItemTextEdit = singleTextRemoveCommonPrefix(suggestItemInfo.getSingleTextEdit(), textModel); const valid = singleTextEditAugments(itemToPreselect, suggestItemTextEdit); return { index, valid, prefixLength: suggestItemTextEdit.text.length, suggestItem }; }) @@ -224,7 +224,7 @@ export class SuggestItemInfo { return new SelectedSuggestionInfo(this.range, this.insertText, this.completionItemKind, this.isSnippetText); } - public toSingleTextEdit(): SingleTextEdit { + public getSingleTextEdit(): SingleTextEdit { return new SingleTextEdit(this.range, this.insertText); } } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts index 8a307df3bd3..a6171e0aa03 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts @@ -18,6 +18,37 @@ export interface IRecordableEditorLogEntry extends IRecordableLogEntry { modelVersion: number; } +export type EditorLogEntryData = IDocumentEventDataSetChangeReason | IDocumentEventFetchStart; +export type LogEntryData = IEventFetchEnd; + +export interface IDocumentEventDataSetChangeReason { + sourceId: 'TextModel.setChangeReason'; + source: 'inlineSuggestion.accept' | 'snippet' | string; + detailedSource?: string; +} + +interface IDocumentEventFetchStart { + sourceId: 'InlineCompletions.fetch'; + kind: 'start'; + requestId: number; +} + +export interface IEventFetchEnd { + sourceId: 'InlineCompletions.fetch'; + kind: 'end'; + requestId: number; + error: string | undefined; + result: IFetchResult[]; +} + +interface IFetchResult { + range: string; + text: string; + isInlineEdit: boolean; + source: string; +} + + /** * The sourceLabel must not contain '@'! */ diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 3d0da6db7e2..bb9c627f3fd 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Permutation, compareBy } from '../../../../base/common/arrays.js'; -import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { CancellationToken } from '../../../../base/common/cancellation.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 +19,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); } @@ -101,3 +81,20 @@ export class ObservableContextKeyService { return bindContextKey(key, this._contextKeyService, obs instanceof Function ? obs : reader => obs.read(reader)); } } + +export function wait(ms: number, cancellationToken?: CancellationToken): Promise { + return new Promise(resolve => { + let d: IDisposable | undefined = undefined; + const handle = setTimeout(() => { + if (d) { d.dispose(); } + resolve(); + }, ms); + if (cancellationToken) { + d = cancellationToken.onCancellationRequested(() => { + clearTimeout(handle); + if (d) { d.dispose(); } + resolve(); + }); + } + }); +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index d592eca09ea..a4215f06f8e 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/code/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/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index bcf120f4996..7437ac73eb9 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -45,7 +45,7 @@ export class InlineCompletionsView extends Disposable { ).recomputeInitiallyAndOnChange(this._store); private readonly _inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineEdit); - private readonly _everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader) || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu); + private readonly _everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader) || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineCompletion?.showInlineEditMenu); protected readonly _inlineEditWidget = derivedDisposable(reader => { if (!this._everHadInlineEdit.read(reader)) { return undefined; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 4e0a4c88323..57d3134c250 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -6,7 +6,7 @@ import { ChildNode, LiveElement, n } from '../../../../../../../base/browser/dom.js'; import { ActionBar, IActionBarOptions } from '../../../../../../../base/browser/ui/actionbar/actionbar.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { KeybindingLabel } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IAction } from '../../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../../../../../base/common/keybindings.js'; @@ -18,7 +18,8 @@ import { ICommandService } from '../../../../../../../platform/commands/common/c import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { nativeHoverDelegate } from '../../../../../../../platform/hover/browser/hover.js'; import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; -import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { defaultKeybindingLabelStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; +import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder, keybindingLabelBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; @@ -192,9 +193,16 @@ function option(props: { }, [ThemeIcon.isThemeIcon(props.icon) ? renderIcon(props.icon) : props.icon.map(icon => renderIcon(icon))]), n.elem('span', {}, [props.title]), n.div({ - style: { marginLeft: 'auto', opacity: '0.6' }, + style: { marginLeft: 'auto' }, ref: elem => { - const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); + const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { + disableTitle: true, + ...defaultKeybindingLabelStyles, + keybindingLabelShadow: undefined, + keybindingLabelBackground: asCssVariable(keybindingLabelBackground), + keybindingLabelBorder: 'transparent', + keybindingLabelBottomBorder: undefined, + })); store.add(autorun(reader => { keybindingLabel.set(props.keybinding.read(reader)); })); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 4a3c4f22e56..fc90fe16e58 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/code/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, ) { @@ -127,6 +82,9 @@ export class InlineEditsGutterIndicator extends Disposable { })); this._register(this._editorObs.editor.onMouseMove((e: IEditorMouseEvent) => { + const state = this._state.get(); + if (state === undefined) { return; } + const el = this._iconRef.element; const rect = el.getBoundingClientRect(); const rectangularArea = Rect.fromLeftTopWidthHeight(rect.left, rect.top, rect.width, rect.height); @@ -143,111 +101,22 @@ 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) { this._editorObs.editor.applyFontInfo(this._indicator.element); } })); - - this._register(autorunWithStore((reader, store) => { - const host = this._host.read(reader); - if (!host) { return; } - store.add(host.onDidAccept(() => { - this._storageService.store('inlineEditsGutterIndicatorUserKind', UserKind.Active, StorageScope.APPLICATION, StorageTarget.USER); - })); - })); - } - - 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([ @@ -324,6 +193,57 @@ export class InlineEditsGutterIndicator extends Disposable { return lineNumber.toString(); }); + private readonly _availableWidthForIcon = derived(this, reader => { + const textModel = this._editorObs.editor.getModel(); + const editor = this._editorObs.editor; + const layout = this._editorObs.layoutInfo.read(reader); + const gutterWidth = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft; + + if (!textModel || gutterWidth <= 0) { + return () => 0; + } + + // no glyph margin => the entire gutter width is available as there is no optimal place to put the icon + if (layout.lineNumbersLeft === 0) { + return () => gutterWidth; + } + + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative || /* likely to flicker */ + lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return () => gutterWidth; + } + + const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const rightOfLineNumber = layout.lineNumbersLeft + layout.lineNumbersWidth; + const totalLines = textModel.getLineCount(); + const totalLinesDigits = (totalLines + 1 /* 0 based to 1 based*/).toString().length; + + const offsetDigits: { + firstLineNumberWithDigitCount: number; + topOfLineNumber: number; + usableWidthLeftOfLineNumber: number; + }[] = []; + + // We only need to pre compute the usable width left of the line number for the first line number with a given digit count + for (let digits = 1; digits <= totalLinesDigits; digits++) { + const firstLineNumberWithDigitCount = 10 ** (digits - 1); + const topOfLineNumber = editor.getTopForLineNumber(firstLineNumberWithDigitCount); + const digitsWidth = digits * w; + const usableWidthLeftOfLineNumber = Math.min(gutterWidth, Math.max(0, rightOfLineNumber - digitsWidth - layout.glyphMarginLeft)); + offsetDigits.push({ firstLineNumberWithDigitCount, topOfLineNumber, usableWidthLeftOfLineNumber }); + } + + return (topOffset: number) => { + for (let i = offsetDigits.length - 1; i >= 0; i--) { + if (topOffset >= offsetDigits[i].topOfLineNumber) { + return offsetDigits[i].usableWidthLeftOfLineNumber; + } + } + throw new BugIndicatingError('Could not find avilable width for icon'); + }; + }); + private readonly _layout = derived(this, reader => { const s = this._state.read(reader); if (!s) { return undefined; } @@ -331,79 +251,111 @@ export class InlineEditsGutterIndicator extends Disposable { const layout = this._editorObs.layoutInfo.read(reader); const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - const bottomPadding = 1; - const leftPadding = 1; - const rightPadding = 1; + const gutterViewPortPadding = 1; - // Entire editor area without sticky scroll - const fullViewPort = Rect.fromLeftTopRightBottom(0, 0, layout.width, layout.height - bottomPadding); - const viewPortWithStickyScroll = fullViewPort.withTop(this._stickyScrollHeight.read(reader)); + // Entire gutter view from top left to bottom right + const gutterWidthWithoutPadding = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - 2 * gutterViewPortPadding; + const gutterHeightWithoutPadding = layout.height - 2 * gutterViewPortPadding; + const gutterViewPortWithStickyScroll = Rect.fromLeftTopWidthHeight(gutterViewPortPadding, gutterViewPortPadding, gutterWidthWithoutPadding, gutterHeightWithoutPadding); + const gutterViewPortWithoutStickyScroll = gutterViewPortWithStickyScroll.withTop(this._stickyScrollHeight.read(reader) + gutterViewPortPadding); // The glyph margin area across all relevant lines - const targetVertRange = s.lineOffsetRange.read(reader); - const targetRect = Rect.fromRanges(OffsetRange.fromTo(leftPadding + layout.glyphMarginLeft, layout.decorationsLeft + layout.decorationsWidth - rightPadding), targetVertRange); + const verticalEditRange = s.lineOffsetRange.read(reader); + const gutterEditArea = Rect.fromRanges(OffsetRange.fromTo(gutterViewPortWithoutStickyScroll.left, gutterViewPortWithoutStickyScroll.right), verticalEditRange); // The gutter view container (pill) + const pillHeight = lineHeight; const pillOffset = this._verticalOffset.read(reader); - let pillRect = targetRect.withHeight(lineHeight).withWidth(22).translateY(pillOffset); - const pillRectMoved = pillRect.moveToBeContainedIn(viewPortWithStickyScroll); - - const rect = targetRect; + const pillFullyDockedRect = gutterEditArea.withHeight(pillHeight).translateY(pillOffset); + const pillIsFullyDocked = gutterViewPortWithoutStickyScroll.containsRect(pillFullyDockedRect); + + // The icon which will be rendered in the pill + const iconNoneDocked = this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); + const iconDocked = derived(reader => { + if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { + return Codicon.check; + } + if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { + return Codicon.keyboardTab; + } + const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; + const editStartLineNumber = s.range.read(reader).startLineNumber; + return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; + }); - // Move pill to be in viewport if it is not - pillRect = (targetRect.containsRect(pillRectMoved)) - ? pillRectMoved - : pillRectMoved.moveToBeContainedIn(fullViewPort.intersect(targetRect.union(fullViewPort.withHeight(lineHeight)))!); //viewPortWithStickyScroll.intersect(rect)!; + const idealIconWidth = 22; + const minimalIconWidth = 16; // codicon size + const iconWidth = (pillRect: Rect) => { + const availableWidth = this._availableWidthForIcon.get()(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; + return Math.max(Math.min(availableWidth, idealIconWidth), minimalIconWidth); + }; - // docked = pill was already in the viewport - const docked = rect.containsRect(pillRect) && viewPortWithStickyScroll.containsRect(pillRect); - let iconDirecion = targetRect.containsRect(pillRect) ? - 'right' as const - : pillRect.top > targetRect.top ? - 'top' as const : - 'bottom' as const; - - // Grow icon the the whole glyph margin area if it is docked - let lineNumberRect = pillRect.withWidth(0); - let iconRect = pillRect; - if (docked && pillRect.top === targetRect.top + pillOffset) { - pillRect = pillRect.withWidth(layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - leftPadding - rightPadding); - lineNumberRect = pillRect.intersectHorizontal(new OffsetRange(0, Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - leftPadding - 1, 0))); - iconRect = iconRect.translateX(lineNumberRect.width); + if (pillIsFullyDocked) { + const pillRect = pillFullyDockedRect; + const lineNumberWidth = Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - gutterViewPortWithStickyScroll.left, 0); + const lineNumberRect = pillRect.withWidth(lineNumberWidth); + const iconWidth = Math.max(Math.min(layout.decorationsWidth, idealIconWidth), minimalIconWidth); + const iconRect = pillRect.withWidth(iconWidth).translateX(lineNumberWidth); + + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + pillRect, + lineNumberRect, + }; } - let icon; - if (docked && (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader))) { - icon = renderIcon(Codicon.check); - iconDirecion = 'right'; - } else { - icon = this._tabAction.read(reader) === InlineEditTabAction.Accept ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight); + const pillPartiallyDockedPossibleArea = gutterViewPortWithStickyScroll.intersect(gutterEditArea); // The area in which the pill could be partially docked + const pillIsPartiallyDocked = pillPartiallyDockedPossibleArea && pillPartiallyDockedPossibleArea.height >= pillHeight; + + if (pillIsPartiallyDocked) { + // pillFullyDockedRect is outside viewport, move it into the viewport under sticky scroll as we prefer the pill to not be on top of the sticky scroll + // then move it into the possible area which will only cause it to move if it has to be rendered on top of the sticky scroll + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithoutStickyScroll).moveToBeContainedIn(pillPartiallyDockedPossibleArea); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + pillRect, + }; } - let rotation = 0; - switch (iconDirecion) { - case 'right': rotation = 0; break; - case 'bottom': rotation = 90; break; - case 'top': rotation = -90; break; - } + // pillFullyDockedRect is outside viewport, so move it into viewport + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithStickyScroll); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + // docked = pill was already in the viewport + const iconDirection = pillRect.top < pillFullyDockedRect.top ? + 'top' as const : + 'bottom' as const; return { - rect, - icon, - rotation, - docked, + gutterEditArea, + icon: iconNoneDocked, + iconDirection, iconRect, pillRect, - lineHeight, - lineNumberRect, }; }); + 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()) { @@ -453,9 +405,11 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', onclick: () => { - const docked = this._layout.map(l => l && l.docked).get(); + const layout = this._layout.get(); + const acceptOnClick = layout?.icon.get() === Codicon.check; + this._editorObs.editor.focus(); - if (docked) { + if (acceptOnClick) { this.model.accept(); } else { this.model.jump(); @@ -472,7 +426,7 @@ export class InlineEditsGutterIndicator extends Disposable { position: 'absolute', background: asCssVariable(inlineEditIndicatorBackground), borderRadius: '4px', - ...rectToProps(reader => layout.read(reader).rect), + ...rectToProps(reader => layout.read(reader).gutterEditArea), } }), n.div({ @@ -484,7 +438,7 @@ export class InlineEditsGutterIndicator extends Disposable { }, style: { cursor: 'pointer', - zIndex: '1000', + zIndex: '20', position: 'absolute', backgroundColor: this._gutterIndicatorStyles.map(v => v.background), ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), @@ -492,7 +446,7 @@ export class InlineEditsGutterIndicator extends Disposable { boxSizing: 'border-box', borderRadius: '4px', display: 'flex', - justifyContent: 'center', + justifyContent: 'flex-end', transition: 'background-color 0.2s ease-in-out, width 0.2s ease-in-out', ...rectToProps(reader => layout.read(reader).pillRect), } @@ -500,11 +454,11 @@ export class InlineEditsGutterIndicator extends Disposable { n.div({ className: 'line-number', style: { - lineHeight: layout.map(l => `${l.lineHeight}px`), - display: layout.map(l => l.lineNumberRect.width > 0 ? 'flex' : 'none'), + lineHeight: layout.map(l => l.lineNumberRect ? l.lineNumberRect.height : 0), + display: layout.map(l => l.lineNumberRect ? 'flex' : 'none'), alignItems: 'center', justifyContent: 'flex-end', - width: layout.map(l => l.lineNumberRect.width), + width: layout.map(l => l.lineNumberRect ? l.lineNumberRect.width : 0), height: '100%', color: this._gutterIndicatorStyles.map(v => v.foreground), } @@ -513,17 +467,26 @@ export class InlineEditsGutterIndicator extends Disposable { ), n.div({ style: { - rotate: layout.map(i => `${i.rotation}deg`), + rotate: layout.map(l => `${getRotationFromDirection(l.iconDirection)}deg`), transition: 'rotate 0.2s ease-in-out', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', - width: layout.map(l => `${l.iconRect.width}px`), + marginRight: layout.map(l => l.pillRect.width - l.iconRect.width - (l.lineNumberRect?.width ?? 0)), + width: layout.map(l => l.iconRect.width), } }, [ - layout.map(i => i.icon), + layout.map((l, reader) => renderIcon(l.icon.read(reader))), ]) ]), ])).keepUpdated(this._store); } + +function getRotationFromDirection(direction: 'top' | 'bottom' | 'right'): number { + switch (direction) { + case 'top': return 90; + case 'bottom': return -90; + case 'right': return 0; + } +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts index 0aac963340b..10394bd1b22 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts @@ -4,23 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import { SingleLineEdit } from '../../../../../common/core/lineEdit.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; import { AbstractText, TextEdit } from '../../../../../common/core/textEdit.js'; import { Command } from '../../../../../common/languages.js'; -import { InlineCompletionItem } from '../../model/provideInlineCompletions.js'; +import { InlineSuggestionItem } from '../../model/inlineSuggestionItem.js'; export class InlineEditWithChanges { - public readonly lineEdit = SingleLineEdit.fromSingleTextEdit(this.edit.toSingle(this.originalText), this.originalText); + public get lineEdit() { + return SingleLineEdit.fromSingleTextEdit(this.edit.toSingle(this.originalText), this.originalText); + } + + public get originalLineRange() { return this.lineEdit.lineRange; } + public get modifiedLineRange() { return this.lineEdit.toLineEdit().getNewLineRanges()[0]; } - public readonly originalLineRange = this.lineEdit.lineRange; - public readonly modifiedLineRange = this.lineEdit.toLineEdit().getNewLineRanges()[0]; + public get displayRange() { + return this.originalText.lineRange.intersect( + this.originalLineRange.join( + LineRange.ofLength(this.originalLineRange.startLineNumber, this.lineEdit.newLines.length) + ) + )!; + } constructor( public readonly originalText: AbstractText, public readonly edit: TextEdit, public readonly cursorPosition: Position, public readonly commands: readonly Command[], - public readonly inlineCompletion: InlineCompletionItem + public readonly inlineCompletion: InlineSuggestionItem ) { } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts index 0a07f09a151..62275ba537a 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts @@ -10,9 +10,9 @@ import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { StringText, TextEdit } from '../../../../../common/core/textEdit.js'; -import { Command } from '../../../../../common/languages.js'; +import { Command, InlineCompletionDisplayLocation } from '../../../../../common/languages.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; -import { InlineCompletionWithUpdatedRange } from '../../model/inlineCompletionsSource.js'; +import { InlineCompletionItem } from '../../model/inlineSuggestionItem.js'; import { IInlineEditHost, IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; @@ -22,6 +22,7 @@ export class InlineEditModel implements IInlineEditModel { readonly displayName: string; readonly extensionCommands: Command[]; + readonly displayLocation: InlineCompletionDisplayLocation | undefined; readonly showCollapsed: IObservable; constructor( @@ -31,8 +32,9 @@ export class InlineEditModel implements IInlineEditModel { ) { this.action = this.inlineEdit.inlineCompletion.action; this.displayName = this.inlineEdit.inlineCompletion.source.provider.displayName ?? localize('inlineEdit', "Inline Edit"); - this.extensionCommands = this.inlineEdit.inlineCompletion.source.inlineCompletions.commands ?? []; + this.extensionCommands = this.inlineEdit.inlineCompletion.source.inlineSuggestions.commands ?? []; + this.displayLocation = this.inlineEdit.inlineCompletion.displayLocation; this.showCollapsed = this._model.showCollapsed; } @@ -50,21 +52,19 @@ export class InlineEditModel implements IInlineEditModel { } handleInlineEditShown() { - this._model.handleInlineEditShown(this.inlineEdit.inlineCompletion); + this._model.handleInlineSuggestionShown(this.inlineEdit.inlineCompletion); } } export class InlineEditHost implements IInlineEditHost { readonly onDidAccept: Event; readonly inAcceptFlow: IObservable; - readonly inPartialAcceptFlow: IObservable; constructor( private readonly _model: InlineCompletionsModel, ) { this.onDidAccept = this._model.onDidAccept; this.inAcceptFlow = this._model.inAcceptFlow; - this.inPartialAcceptFlow = this._model.inPartialAcceptFlow; } } @@ -76,12 +76,12 @@ export class GhostTextIndicator { editor: ICodeEditor, model: InlineCompletionsModel, readonly lineRange: LineRange, - inlineCompletion: InlineCompletionWithUpdatedRange, + inlineCompletion: InlineCompletionItem, ) { const editorObs = observableCodeEditor(editor); const tabAction = derived(this, reader => { if (editorObs.isFocused.read(reader)) { - if (model.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) { + if (inlineCompletion.showInlineEditMenu) { return InlineEditTabAction.Accept; } } @@ -92,10 +92,10 @@ export class GhostTextIndicator { model, new InlineEditWithChanges( new StringText(''), - new TextEdit([]), + new TextEdit([inlineCompletion.getSingleTextEdit()]), model.primaryPosition.get(), - inlineCompletion.source.inlineCompletions.commands ?? [], - inlineCompletion.inlineCompletion + inlineCompletion.source.inlineSuggestions.commands ?? [], + inlineCompletion ), tabAction, ); diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts new file mode 100644 index 00000000000..c6ebf022433 --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { 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.identity.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 + this._disposables.value = this.setupNewUserExperience(); + + this._setupDone.set(true, undefined); + } + + private setupNewUserExperience(): IDisposable | undefined { + if (this.getNewUserType() === UserKind.Active) { + return undefined; + } + + 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++ >= 3 && 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); + this._disposables.value = this.setupNewUserExperience(); + } + }); + + return disposable; + } +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index 7931a86e959..cdd3bd12518 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -22,8 +22,10 @@ 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 { InlineEditsCustomView } from './inlineEditsViews/inlineEditsCustomView.js'; import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js'; import { InlineEditsInsertionView } from './inlineEditsViews/inlineEditsInsertionView.js'; import { InlineEditsLineReplacementView } from './inlineEditsViews/inlineEditsLineReplacementView.js'; @@ -77,6 +79,7 @@ export class InlineEditsView extends Disposable { this._insertion.onDidClick, ...this._wordReplacementViews.read(reader).map(w => w.onDidClick), this._inlineDiffView.onDidClick, + this._customView.onDidClick, )(e => { if (this._viewHasBeenShownLongerThan(350)) { e.preventDefault(); @@ -90,6 +93,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 } @@ -101,7 +107,6 @@ export class InlineEditsView extends Disposable { edit: InlineEditWithChanges; newText: string; newTextLineCount: number; - originalDisplayRange: LineRange; } | undefined>(this, reader => { const model = this._model.read(reader); if (!model || !this._constructorDone.read(reader)) { @@ -115,13 +120,7 @@ export class InlineEditsView extends Disposable { let newText = inlineEdit.edit.apply(inlineEdit.originalText); let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); - const originalDisplayRange = inlineEdit.originalText.lineRange.intersect( - inlineEdit.originalLineRange.join( - LineRange.ofLength(inlineEdit.originalLineRange.startLineNumber, inlineEdit.lineEdit.newLines.length) - ) - )!; - - let state = this.determineRenderState(model, reader, diff, new StringText(newText), originalDisplayRange); + let state = this.determineRenderState(model, reader, diff, new StringText(newText)); if (!state) { model.abort(`unable to determine view: tried to render ${this._previousView?.view}`); return undefined; @@ -153,7 +152,6 @@ export class InlineEditsView extends Disposable { edit: inlineEdit, newText, newTextLineCount: inlineEdit.modifiedLineRange.length, - originalDisplayRange: originalDisplayRange, }; }); @@ -179,10 +177,21 @@ export class InlineEditsView extends Disposable { } const state = this._uiState.read(reader); - if (state?.state?.kind === 'insertionMultiLine') { + if (!state) { return undefined; } + + if (state.state?.kind === 'custom') { + const range = state.state.displayLocation?.range; + if (!range) { + throw new BugIndicatingError('custom view should have a range'); + } + return new LineRange(range.startLineNumber, range.endLineNumber); + } + + if (state.state?.kind === 'insertionMultiLine') { return this._insertion.originalLines.read(reader); } - return state?.originalDisplayRange; + + return state.edit.displayRange; }); const modelWithGhostTextSupport = derived(this, reader => { @@ -204,7 +213,6 @@ export class InlineEditsView extends Disposable { this._editorObs, indicatorDisplayRange, this._gutterIndicatorOffset, - this._host, modelWithGhostTextSupport, this._inlineEditsIsHovered, this._focusIsInMenu, @@ -217,7 +225,8 @@ export class InlineEditsView extends Disposable { || this._deletion.isHovered.read(reader) || this._inlineDiffView.isHovered.read(reader) || this._lineReplacementView.isHovered.read(reader) - || this._insertion.isHovered.read(reader); + || this._insertion.isHovered.read(reader) + || this._customView.isHovered.read(reader); }); private readonly _gutterIndicatorOffset = derived(this, reader => { @@ -234,7 +243,6 @@ export class InlineEditsView extends Disposable { this._previewTextModel, this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ newTextLineCount: s.newTextLineCount, - originalDisplayRange: s.originalDisplayRange, }) : undefined), this._tabAction, )); @@ -262,7 +270,7 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e || !e.state) { return undefined; } - if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed') { + if (e.state.kind === 'wordReplacements' || e.state.kind === 'lineReplacement' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed' || e.state.kind === 'custom') { return undefined; } return { @@ -278,6 +286,12 @@ export class InlineEditsView extends Disposable { this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'collapsed' ? m?.inlineEdit : undefined) )); + protected readonly _customView = this._register(this._instantiationService.createInstance(InlineEditsCustomView, + this._editor, + this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'custom' ? m?.displayLocation : undefined), + this._tabAction, + )); + protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { @@ -296,15 +310,10 @@ export class InlineEditsView extends Disposable { )); private getCacheId(model: IInlineEditModel) { - const inlineEdit = model.inlineEdit; - if (this._host.get()?.inPartialAcceptFlow.get()) { - return `${inlineEdit.inlineCompletion.id}_${inlineEdit.edit.edits.map(innerEdit => innerEdit.range.toString() + innerEdit.text).join(',')}`; - } - - return inlineEdit.inlineCompletion.id; + return model.inlineEdit.inlineCompletion.identity.id; } - private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string { + private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): string { // Check if we can use the previous view if it is the same InlineCompletion as previously shown const inlineEdit = model.inlineEdit; const canUseCache = this._previousView?.id === this.getCacheId(model); @@ -318,6 +327,10 @@ export class InlineEditsView extends Disposable { return this._previousView!.view; } + if (model.displayLocation) { + return 'custom'; + } + // Determine the view based on the edit / diff const inner = diff.flatMap(d => d.innerChanges ?? []); @@ -351,8 +364,13 @@ export class InlineEditsView extends Disposable { return 'wordReplacements'; } } + if (numOriginalLines > 0 && numModifiedLines > 0) { - if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, this._previewTextModel, inlineEdit, originalDisplayRange, reader)) { + if (numOriginalLines === 1 && numModifiedLines === 1) { + return 'lineReplacement'; + } + + if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, this._previewTextModel, inlineEdit, reader)) { return 'sideBySide'; } @@ -362,14 +380,15 @@ export class InlineEditsView extends Disposable { return 'sideBySide'; } - private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) { + private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { const inlineEdit = model.inlineEdit; - const view = this.determineView(model, reader, diff, newText, originalDisplayRange); + const view = this.determineView(model, reader, diff, newText); this._previousView = { id: this.getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() }; switch (view) { + case 'custom': return { kind: 'custom' as const, displayLocation: model.displayLocation }; case 'insertionInline': return { kind: 'insertionInline' as const }; case 'sideBySide': return { kind: 'sideBySide' as const }; case 'collapsed': return { kind: 'collapsed' as const }; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts index dae493157af..0e2d21c8540 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts @@ -6,7 +6,7 @@ import { IMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; import { Event } from '../../../../../../base/common/event.js'; import { IObservable } from '../../../../../../base/common/observable.js'; -import { Command } from '../../../../../common/languages.js'; +import { Command, InlineCompletionDisplayLocation } from '../../../../../common/languages.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; export enum InlineEditTabAction { @@ -21,8 +21,8 @@ export interface IInlineEditsView { } export interface IInlineEditHost { + readonly onDidAccept: Event; inAcceptFlow: IObservable; - inPartialAcceptFlow: IObservable; } export interface IInlineEditModel { @@ -32,6 +32,7 @@ export interface IInlineEditModel { inlineEdit: InlineEditWithChanges; tabAction: IObservable; showCollapsed: IObservable; + displayLocation: InlineCompletionDisplayLocation | undefined; handleInlineEditShown(): void; accept(): void; diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts index 39b6acae41f..69e0c7c1b32 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts @@ -33,11 +33,10 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const textModel = this._editor.getModel(); if (!textModel) { return undefined; } - const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit.read(reader); + const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit; if (!editOffset) { return undefined; } - const offsetEdits = model.inPartialAcceptFlow.read(reader) ? [editOffset.edits[0]] : editOffset.edits; - const edits = offsetEdits.map(e => { + const edits = editOffset.edits.map(e => { const innerEditRange = Range.fromPositions( textModel.getPositionAt(e.replaceRange.start), textModel.getPositionAt(e.replaceRange.endExclusive) @@ -82,7 +81,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const inlineCompletion = state.inlineCompletion; if (!inlineCompletion) { return undefined; } - if (!inlineCompletion.sourceInlineCompletion.showInlineEditMenu) { + if (!inlineCompletion.showInlineEditMenu) { return undefined; } diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts index 2acbdd9d729..458aacf7533 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts +++ b/code/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(); @@ -52,7 +57,6 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: 'block', }, }, [ @@ -65,6 +69,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 +99,7 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits return n.svg({ class: 'collapsedView', + ref: this._iconRef, style: { position: 'absolute', top: 0, diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts new file mode 100644 index 00000000000..16fad4d39ac --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { getWindow, n } from '../../../../../../../base/browser/dom.js'; +import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { autorun, constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; +import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; +import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; +import { Rect } from '../../../../../../browser/rect.js'; +import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; +import { EditorOption } from '../../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../../common/core/lineRange.js'; +import { InlineCompletionDisplayLocation } from '../../../../../../common/languages.js'; +import { ILanguageService } from '../../../../../../common/languages/language.js'; +import { LineTokens } from '../../../../../../common/tokens/lineTokens.js'; +import { TokenArray } from '../../../../../../common/tokens/tokenArray.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBlendedColor, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorsuccessfulBackground } from '../theme.js'; +import { maxContentWidthInRange, rectToProps } from '../utils/utils.js'; + +export class InlineEditsCustomView extends Disposable implements IInlineEditsView { + + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; + + private readonly _isHovered = observableValue(this, false); + readonly isHovered: IObservable = this._isHovered; + private readonly _viewRef = n.ref(); + + private readonly _editorObs: ObservableCodeEditor; + + constructor( + private readonly _editor: ICodeEditor, + displayLocation: IObservable, + tabAction: IObservable, + @IThemeService themeService: IThemeService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._editorObs = observableCodeEditor(this._editor); + + /* const styles = derived(reader => ({ + background: getEditorBlendedColor(modifiedChangedLineBackgroundColor, themeService).read(reader).toString(), + border: asCssVariable(getModifiedBorderColor(tabAction).read(reader)), + })); */ + + const styles = tabAction.map((v, reader) => { + let border; + switch (v) { + case InlineEditTabAction.Inactive: border = inlineEditIndicatorSecondaryBackground; break; + case InlineEditTabAction.Jump: border = inlineEditIndicatorPrimaryBackground; break; + case InlineEditTabAction.Accept: border = inlineEditIndicatorsuccessfulBackground; break; + } + return { + border: getEditorBlendedColor(border, themeService).read(reader).toString(), + background: asCssVariable(editorBackground) + }; + }); + + /* const styles = derived(reader => ({ + background: asCssVariable(editorBackground), + border: asCssVariable(getModifiedBorderColor(tabAction).read(reader)), + })); */ + + const state = displayLocation.map(dl => dl ? this.getState(dl) : undefined); + + const view = state.map(s => s ? this.getRendering(s, styles) : undefined); + + const overlay = n.div({ + class: 'inline-edits-custom-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + display: 'block', + }, + }, [view]).keepUpdated(this._store); + + this._register(this._editorObs.createOverlayWidget({ + domNode: overlay.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + + this._register(autorun((reader) => { + const v = view.read(reader); + if (!v) { this._isHovered.set(false, undefined); return; } + this._isHovered.set(overlay.isHovered.read(reader), undefined); + })); + } + + private getState(displayLocation: InlineCompletionDisplayLocation): { rect: IObservable; label: string } { + + const contentState = derived((reader) => { + const startLineNumber = displayLocation.range.startLineNumber; + const endLineNumber = displayLocation.range.endLineNumber; + const startColumn = displayLocation.range.startColumn; + const endColumn = displayLocation.range.endColumn; + const lineCount = this._editor.getModel()?.getLineCount() ?? 0; + + const lineWidth = maxContentWidthInRange(this._editorObs, new LineRange(startLineNumber, startLineNumber + 1), reader); + const lineWidthBelow = startLineNumber + 1 <= lineCount ? maxContentWidthInRange(this._editorObs, new LineRange(startLineNumber + 1, startLineNumber + 2), reader) : undefined; + const lineWidthAbove = startLineNumber - 1 >= 1 ? maxContentWidthInRange(this._editorObs, new LineRange(startLineNumber - 1, startLineNumber), reader) : undefined; + const startContentLeftOffset = this._editor.getOffsetForColumn(startLineNumber, startColumn); + const endContentLeftOffset = this._editor.getOffsetForColumn(endLineNumber, endColumn); + + return { + lineWidth, + lineWidthBelow, + lineWidthAbove, + startContentLeftOffset, + endContentLeftOffset + }; + }); + + const minEndOfLinePadding = 14; + const paddingVertically = 0; + const paddingHorizontally = 4; + const horizontalOffsetWhenAboveBelow = 4; + const verticalOffsetWhenAboveBelow = 2; + // !! minEndOfLinePadding should always be larger than paddingHorizontally + horizontalOffsetWhenAboveBelow + + const rect = derived((reader) => { + const w = this._editorObs.getOption(EditorOption.fontInfo).read(reader).typicalHalfwidthCharacterWidth; + + const startLineNumber = displayLocation.range.startLineNumber; + const endLineNumber = displayLocation.range.endLineNumber; + const { lineWidth, lineWidthBelow, lineWidthAbove, startContentLeftOffset, endContentLeftOffset } = contentState.read(reader); + + const contentLeft = this._editorObs.layoutInfoContentLeft.read(reader); + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + const scrollTop = this._editorObs.scrollTop.read(reader); + const scrollLeft = this._editorObs.scrollLeft.read(reader); + + let position: 'end' | 'below' | 'above'; + if (startLineNumber === endLineNumber && endContentLeftOffset + 5 * w >= lineWidth) { + position = 'end'; // Render at the end of the line if the range ends almost at the end of the line + } else if (lineWidthBelow !== undefined && lineWidthBelow + minEndOfLinePadding - horizontalOffsetWhenAboveBelow - paddingHorizontally < startContentLeftOffset) { + position = 'below'; // Render Below if possible + } else if (lineWidthAbove !== undefined && lineWidthAbove + minEndOfLinePadding - horizontalOffsetWhenAboveBelow - paddingHorizontally < startContentLeftOffset) { + position = 'above'; // Render Above if possible + } else { + position = 'end'; // Render at the end of the line otherwise + } + + let topOfLine; + let contentStartOffset; + let deltaX = 0; + let deltaY = 0; + + switch (position) { + case 'end': { + topOfLine = this._editorObs.editor.getTopForLineNumber(startLineNumber); + contentStartOffset = lineWidth; + deltaX = paddingHorizontally + minEndOfLinePadding; + break; + } + case 'below': { + topOfLine = this._editorObs.editor.getTopForLineNumber(startLineNumber + 1); + contentStartOffset = startContentLeftOffset; + deltaX = paddingHorizontally + horizontalOffsetWhenAboveBelow; + deltaY = paddingVertically + verticalOffsetWhenAboveBelow; + break; + } + case 'above': { + topOfLine = this._editorObs.editor.getTopForLineNumber(startLineNumber - 1); + contentStartOffset = startContentLeftOffset; + deltaX = paddingHorizontally + horizontalOffsetWhenAboveBelow; + deltaY = -paddingVertically + verticalOffsetWhenAboveBelow; + break; + } + } + + const textRect = Rect.fromLeftTopWidthHeight( + contentLeft + contentStartOffset - scrollLeft, + topOfLine - scrollTop, + w * displayLocation.label.length, + lineHeight + ); + + return textRect.withMargin(paddingVertically, paddingHorizontally).translateX(deltaX).translateY(deltaY); + }); + + return { + rect, + label: displayLocation.label + }; + } + + private getRendering(state: { rect: IObservable; label: string }, styles: IObservable<{ background: string; border: string }>) { + + const line = document.createElement('div'); + const t = this._editor.getModel()!.tokenization.tokenizeLinesAt(1, [state.label])?.[0]; + let tokens: LineTokens; + if (t) { + tokens = TokenArray.fromLineTokens(t).toLineTokens(state.label, this._languageService.languageIdCodec); + } else { + tokens = LineTokens.createEmpty(state.label, this._languageService.languageIdCodec); + } + + const result = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor).withSetWidth(false).withScrollBeyondLastColumn(0), [], line, true); + line.style.width = `${result.minWidthInPx}px`; + + const rect = state.rect.map(r => r.withMargin(0, 4)); + + return n.div({ + class: 'collapsedView', + ref: this._viewRef, + style: { + position: 'absolute', + ...rectToProps(reader => rect.read(reader)), + overflow: 'hidden', + boxSizing: 'border-box', + cursor: 'pointer', + border: styles.map(s => `1px solid ${s.border}`), + borderRadius: '4px', + backgroundColor: styles.map(s => s.background), + + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + whiteSpace: 'nowrap', + }, + onclick: (e) => { this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); } + }, [ + line + ]); + } +} diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts index fcd48453e74..253d45b1ea5 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts @@ -198,7 +198,6 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index e79107bbd24..6cafdf6f116 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -302,7 +302,6 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index 7d79399b099..0d5e96cee9f 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -222,7 +222,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).lowerBackground.translateX(-contentLeft)), - borderRadius: '4px', + borderRadius: '0 0 4px 4px', background: asCssVariable(editorBackground), boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, border: `1px solid ${asCssVariable(modifiedBorderColor)}`, @@ -254,7 +254,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin fontWeight: this._editor.getOption(EditorOption.fontWeight), pointerEvents: 'none', whiteSpace: 'nowrap', - borderRadius: '4px', + borderRadius: '0 0 4px 4px', overflow: 'hidden', } }, [...modifiedLineElements.lines]), diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index 26e186b6c88..9e30a4860dd 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -17,7 +17,6 @@ import { observableCodeEditor } from '../../../../../../browser/observableCodeEd import { Rect } from '../../../../../../browser/rect.js'; import { EmbeddedCodeEditorWidget } from '../../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { LineRange } from '../../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; @@ -42,14 +41,14 @@ const MODIFIED_END_PADDING = 12; export class InlineEditsSideBySideView extends Disposable implements IInlineEditsView { // This is an approximation and should be improved by using the real parameters used bellow - static fitsInsideViewport(editor: ICodeEditor, textModel: ITextModel, edit: InlineEditWithChanges, originalDisplayRange: LineRange, reader: IReader): boolean { + static fitsInsideViewport(editor: ICodeEditor, textModel: ITextModel, edit: InlineEditWithChanges, reader: IReader): boolean { const editorObs = observableCodeEditor(editor); const editorWidth = editorObs.layoutInfoWidth.read(reader); const editorContentLeft = editorObs.layoutInfoContentLeft.read(reader); const editorVerticalScrollbar = editor.getLayoutInfo().verticalScrollbarWidth; const minimapWidth = editorObs.layoutInfoMinimap.read(reader).minimapLeft !== 0 ? editorObs.layoutInfoMinimap.read(reader).minimapWidth : 0; - const maxOriginalContent = maxContentWidthInRange(editorObs, originalDisplayRange, undefined/* do not reconsider on each layout info change */); + const maxOriginalContent = maxContentWidthInRange(editorObs, edit.displayRange, undefined/* do not reconsider on each layout info change */); const maxModifiedContent = edit.lineEdit.newLines.reduce((max, line) => Math.max(max, getContentRenderWidth(line, editor, textModel)), 0); const originalPadding = ORIGINAL_END_PADDING; // padding after last line of original editor const modifiedPadding = MODIFIED_END_PADDING + 2 * BORDER_WIDTH; // padding after last line of modified editor @@ -68,7 +67,6 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _previewTextModel: ITextModel, private readonly _uiState: IObservable<{ newTextLineCount: number; - originalDisplayRange: LineRange; } | undefined>, private readonly _tabAction: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -260,7 +258,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); - private readonly _originalDisplayRange = this._uiState.map(s => s?.originalDisplayRange); + private readonly _originalDisplayRange = this._edit.map(e => e?.displayRange); private readonly _editorMaxContentWidthInRange = derived(this, reader => { const originalDisplayRange = this._originalDisplayRange.read(reader); if (!originalDisplayRange) { @@ -583,7 +581,6 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index 656469803f8..ff04b209adc 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/code/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/code/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts new file mode 100644 index 00000000000..c6e68393ab1 --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Range } from '../../../../common/core/range.js'; +import { SingleTextEdit } from '../../../../common/core/textEdit.js'; +import { createTextModel } from '../../../../test/common/testTextModel.js'; +import { computeGhostText } from '../../browser/model/computeGhostText.js'; + +suite('computeGhostText', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function getOutput(text: string, suggestion: string): unknown { + const rangeStartOffset = text.indexOf('['); + const rangeEndOffset = text.indexOf(']') - 1; + const cleanedText = text.replace('[', '').replace(']', ''); + const tempModel = createTextModel(cleanedText); + const range = Range.fromPositions(tempModel.getPositionAt(rangeStartOffset), tempModel.getPositionAt(rangeEndOffset)); + const options = ['prefix', 'subword'] as const; + const result = {} as any; + for (const option of options) { + result[option] = computeGhostText(new SingleTextEdit(range, suggestion), tempModel, option)?.render(cleanedText, true); + } + + tempModel.dispose(); + + if (new Set(Object.values(result)).size === 1) { + return Object.values(result)[0]; + } + + return result; + } + + test('Basic', () => { + assert.deepStrictEqual(getOutput('[foo]baz', 'foobar'), 'foo[bar]baz'); + assert.deepStrictEqual(getOutput('[aaa]aaa', 'aaaaaa'), 'aaa[aaa]aaa'); + assert.deepStrictEqual(getOutput('[foo]baz', 'boobar'), undefined); + assert.deepStrictEqual(getOutput('[foo]foo', 'foofoo'), 'foo[foo]foo'); + assert.deepStrictEqual(getOutput('foo[]', 'bar\nhello'), 'foo[bar\nhello]'); + }); + + test('Empty ghost text', () => { + assert.deepStrictEqual(getOutput('[foo]', 'foo'), 'foo'); + }); + + test('Whitespace (indentation)', () => { + assert.deepStrictEqual(getOutput('[ foo]', 'foobar'), ' foo[bar]'); + assert.deepStrictEqual(getOutput('[\tfoo]', 'foobar'), '\tfoo[bar]'); + assert.deepStrictEqual(getOutput('[\t foo]', '\tfoobar'), ' foo[bar]'); + assert.deepStrictEqual(getOutput('[\tfoo]', '\t\tfoobar'), { prefix: undefined, subword: '\t[\t]foo[bar]' }); + assert.deepStrictEqual(getOutput('[\t]', '\t\tfoobar'), '\t[\tfoobar]'); + assert.deepStrictEqual(getOutput('\t[]', '\t'), '\t[\t]'); + assert.deepStrictEqual(getOutput('\t[\t]', ''), '\t\t'); + + assert.deepStrictEqual(getOutput('[ ]', 'return 1'), ' [return 1]'); + }); + + test('Whitespace (outside of indentation)', () => { + assert.deepStrictEqual(getOutput('bar[ foo]', 'foobar'), undefined); + assert.deepStrictEqual(getOutput('bar[\tfoo]', 'foobar'), undefined); + }); + + test('Unsupported Case', () => { + assert.deepStrictEqual(getOutput('fo[o\n]', 'x\nbar'), undefined); + }); + + test('New Line', () => { + assert.deepStrictEqual(getOutput('fo[o\n]', 'o\nbar'), 'foo\n[bar]'); + }); + + test('Multi Part Diffing', () => { + assert.deepStrictEqual(getOutput('foo[()]', '(x);'), { prefix: undefined, subword: 'foo([x])[;]' }); + assert.deepStrictEqual(getOutput('[\tfoo]', '\t\tfoobar'), { prefix: undefined, subword: '\t[\t]foo[bar]' }); + assert.deepStrictEqual(getOutput('[(y ===)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ===[ 1])[ { f(); }]' }); + assert.deepStrictEqual(getOutput('[(y ==)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ==[= 1])[ { f(); }]' }); + + assert.deepStrictEqual(getOutput('[(y ==)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ==[= 1])[ { f(); }]' }); + }); + + test('Multi Part Diffing 1', () => { + assert.deepStrictEqual(getOutput('[if () ()]', 'if (1 == f()) ()'), { prefix: undefined, subword: 'if ([1 == f()]) ()' }); + }); + + test('Multi Part Diffing 2', () => { + assert.deepStrictEqual(getOutput('[)]', '())'), ({ prefix: undefined, subword: "[(])[)]" })); + assert.deepStrictEqual(getOutput('[))]', '(())'), ({ prefix: undefined, subword: "[((]))" })); + }); + + test('Parenthesis Matching', () => { + assert.deepStrictEqual(getOutput('[console.log()]', 'console.log({ label: "(" })'), { + prefix: undefined, + subword: 'console.log([{ label: "(" }])' + }); + }); +}); diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts similarity index 90% rename from code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts rename to code/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts index 6ba5f06b158..b418f100b03 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/getSecondaryEdits.test.ts @@ -10,11 +10,11 @@ import { createTextModel } from '../../../../test/common/testTextModel.js'; import { Range } from '../../../../common/core/range.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -suite('inlineCompletionModel', () => { +suite('getSecondaryEdits', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('getSecondaryEdits - basic', async function () { + test('basic', async function () { const textModel = createTextModel([ 'function fib(', @@ -33,7 +33,7 @@ suite('inlineCompletionModel', () => { textModel.dispose(); }); - test('getSecondaryEdits - cursor not on same line as primary edit 1', async function () { + test('cursor not on same line as primary edit 1', async function () { const textModel = createTextModel([ 'function fib(', @@ -60,7 +60,7 @@ suite('inlineCompletionModel', () => { textModel.dispose(); }); - test('getSecondaryEdits - cursor not on same line as primary edit 2', async function () { + test('cursor not on same line as primary edit 2', async function () { const textModel = createTextModel([ 'class A {', diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts similarity index 51% rename from code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts rename to code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts index 28bd5ab66a4..e1266b26613 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts @@ -5,114 +5,16 @@ import assert from 'assert'; import { timeout } from '../../../../../base/common/async.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Range } from '../../../../common/core/range.js'; -import { InlineCompletionsProvider } from '../../../../common/languages.js'; -import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; -import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js'; -import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js'; -import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js'; import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js'; -import { SingleTextEdit } from '../../../../common/core/textEdit.js'; -import { GhostTextContext, MockInlineCompletionsProvider } from './utils.js'; -import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; -import { createTextModel } from '../../../../test/common/testTextModel.js'; -import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { IWithAsyncTestCodeEditorAndInlineCompletionsModel, MockInlineCompletionsProvider, withAsyncTestCodeEditorAndInlineCompletionsModel } from './utils.js'; +import { ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { Selection } from '../../../../common/core/selection.js'; -import { computeGhostText } from '../../browser/model/computeGhostText.js'; suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite(); - suite('inlineCompletionToGhostText', () => { - - function getOutput(text: string, suggestion: string): unknown { - const rangeStartOffset = text.indexOf('['); - const rangeEndOffset = text.indexOf(']') - 1; - const cleanedText = text.replace('[', '').replace(']', ''); - const tempModel = createTextModel(cleanedText); - const range = Range.fromPositions(tempModel.getPositionAt(rangeStartOffset), tempModel.getPositionAt(rangeEndOffset)); - const options = ['prefix', 'subword'] as const; - const result = {} as any; - for (const option of options) { - result[option] = computeGhostText(new SingleTextEdit(range, suggestion), tempModel, option)?.render(cleanedText, true); - } - - tempModel.dispose(); - - if (new Set(Object.values(result)).size === 1) { - return Object.values(result)[0]; - } - - return result; - } - - test('Basic', () => { - assert.deepStrictEqual(getOutput('[foo]baz', 'foobar'), 'foo[bar]baz'); - assert.deepStrictEqual(getOutput('[aaa]aaa', 'aaaaaa'), 'aaa[aaa]aaa'); - assert.deepStrictEqual(getOutput('[foo]baz', 'boobar'), undefined); - assert.deepStrictEqual(getOutput('[foo]foo', 'foofoo'), 'foo[foo]foo'); - assert.deepStrictEqual(getOutput('foo[]', 'bar\nhello'), 'foo[bar\nhello]'); - }); - - test('Empty ghost text', () => { - assert.deepStrictEqual(getOutput('[foo]', 'foo'), 'foo'); - }); - - test('Whitespace (indentation)', () => { - assert.deepStrictEqual(getOutput('[ foo]', 'foobar'), ' foo[bar]'); - assert.deepStrictEqual(getOutput('[\tfoo]', 'foobar'), '\tfoo[bar]'); - assert.deepStrictEqual(getOutput('[\t foo]', '\tfoobar'), ' foo[bar]'); - assert.deepStrictEqual(getOutput('[\tfoo]', '\t\tfoobar'), { prefix: undefined, subword: '\t[\t]foo[bar]' }); - assert.deepStrictEqual(getOutput('[\t]', '\t\tfoobar'), '\t[\tfoobar]'); - assert.deepStrictEqual(getOutput('\t[]', '\t'), '\t[\t]'); - assert.deepStrictEqual(getOutput('\t[\t]', ''), '\t\t'); - - assert.deepStrictEqual(getOutput('[ ]', 'return 1'), ' [return 1]'); - }); - - test('Whitespace (outside of indentation)', () => { - assert.deepStrictEqual(getOutput('bar[ foo]', 'foobar'), undefined); - assert.deepStrictEqual(getOutput('bar[\tfoo]', 'foobar'), undefined); - }); - - test('Unsupported Case', () => { - assert.deepStrictEqual(getOutput('fo[o\n]', 'x\nbar'), undefined); - }); - - test('New Line', () => { - assert.deepStrictEqual(getOutput('fo[o\n]', 'o\nbar'), 'foo\n[bar]'); - }); - - test('Multi Part Diffing', () => { - assert.deepStrictEqual(getOutput('foo[()]', '(x);'), { prefix: undefined, subword: 'foo([x])[;]' }); - assert.deepStrictEqual(getOutput('[\tfoo]', '\t\tfoobar'), { prefix: undefined, subword: '\t[\t]foo[bar]' }); - assert.deepStrictEqual(getOutput('[(y ===)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ===[ 1])[ { f(); }]' }); - assert.deepStrictEqual(getOutput('[(y ==)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ==[= 1])[ { f(); }]' }); - - assert.deepStrictEqual(getOutput('[(y ==)]', '(y === 1) { f(); }'), { prefix: undefined, subword: '(y ==[= 1])[ { f(); }]' }); - }); - - test('Multi Part Diffing 1', () => { - assert.deepStrictEqual(getOutput('[if () ()]', 'if (1 == f()) ()'), { prefix: undefined, subword: 'if ([1 == f()]) ()' }); - }); - - test('Multi Part Diffing 2', () => { - assert.deepStrictEqual(getOutput('[)]', '())'), ({ prefix: undefined, subword: "[(])[)]" })); - assert.deepStrictEqual(getOutput('[))]', '(())'), ({ prefix: undefined, subword: "[((]))" })); - }); - - test('Parenthesis Matching', () => { - assert.deepStrictEqual(getOutput('[console.log()]', 'console.log({ label: "(" })'), { - prefix: undefined, - subword: 'console.log([{ label: "(" }])' - }); - }); - }); - test('Does not trigger automatically if disabled', async function () { const provider = new MockInlineCompletionsProvider(); await withAsyncTestCodeEditorAndInlineCompletionsModel('', @@ -366,100 +268,158 @@ suite('Inline Completions', () => { ); }); - test('Forward stability', async function () { - // The user types the text as suggested and the provider is forward-stable - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); - context.keyboardType('foo'); - model.trigger(); - await timeout(1000); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,4)', text: 'foo', triggerKind: 0, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'foo[bar]']); - provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 5) }); - context.keyboardType('b'); - assert.deepStrictEqual(context.currentPrettyViewState, 'foob[ar]'); - await timeout(1000); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,5)', text: 'foob', triggerKind: 0, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['foob[ar]']); + suite('Forward Stability', () => { + test('Typing agrees', async function () { + // The user types the text as suggested and the provider is forward-stable + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + provider.setReturnValue({ insertText: 'foobar', }); + context.keyboardType('foo'); + model.trigger(); + await timeout(1000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,4)', text: 'foo', triggerKind: 0, } + ]); + assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'foo[bar]']); - provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 6) }); - context.keyboardType('a'); - assert.deepStrictEqual(context.currentPrettyViewState, 'fooba[r]'); - await timeout(1000); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,6)', text: 'fooba', triggerKind: 0, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['fooba[r]']); - } - ); - }); + context.keyboardType('b'); + assert.deepStrictEqual(context.getAndClearViewStates(), (["foob[ar]"])); + await timeout(1000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,5)', text: 'foob', triggerKind: 0, } + ]); + assert.deepStrictEqual(context.getAndClearViewStates(), []); - test('Support forward instability', async function () { - // The user types the text as suggested and the provider reports a different suggestion. + context.keyboardType('a'); + assert.deepStrictEqual(context.getAndClearViewStates(), (["fooba[r]"])); + await timeout(1000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,6)', text: 'fooba', triggerKind: 0, } + ]); + assert.deepStrictEqual(context.getAndClearViewStates(), []); + } + ); + }); - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); - context.keyboardType('foo'); - model.triggerExplicitly(); - await timeout(100); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,4)', text: 'foo', triggerKind: 1, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'foo[bar]']); + async function setupScenario({ editor, editorViewModel, model, context, store }: IWithAsyncTestCodeEditorAndInlineCompletionsModel, provider: MockInlineCompletionsProvider): Promise { + assert.deepStrictEqual(context.getAndClearViewStates(), ['']); + provider.setReturnValue({ insertText: 'foo bar' }); + context.keyboardType('f'); + model.triggerExplicitly(); + await timeout(10000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(1,2)", triggerKind: 1, text: "f" }])); + assert.deepStrictEqual(context.getAndClearViewStates(), (["f[oo bar]"])); + + provider.setReturnValue({ insertText: 'foo baz' }); + await timeout(10000); + } - provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 5) }); - context.keyboardType('b'); - assert.deepStrictEqual(context.currentPrettyViewState, 'foob[ar]'); - await timeout(100); - // This behavior might change! - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,5)', text: 'foob', triggerKind: 0, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['foob[ar]', 'foob[az]']); - } - ); - }); + test('Support forward instability', async function () { + // The user types the text as suggested and the provider reports a different suggestion. + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async (ctx) => { + await setupScenario(ctx, provider); - test('Support backward instability', async function () { - // The user deletes text and the suggestion changes - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('fooba'); + ctx.context.keyboardType('o'); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), ['fo[o bar]']); + await timeout(10000); - provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 6) }); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,3)', text: 'fo', triggerKind: 0, } + ]); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), ['fo[o baz]']); + } + ); + }); - model.triggerExplicitly(); - await timeout(1000); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,6)', text: 'fooba', triggerKind: 1, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'fooba[r]']); - provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 5) }); - context.leftDelete(); - await timeout(1000); - assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { position: '(1,5)', text: 'foob', triggerKind: 0, } - ]); - assert.deepStrictEqual(context.getAndClearViewStates(), [ - 'foob[ar]', - 'foob[az]' - ]); - } - ); + test('when accepting word by word', async function () { + // The user types the text as suggested and the provider reports a different suggestion. + + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async (ctx) => { + await setupScenario(ctx, provider); + + await ctx.model.acceptNextWord(); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["foo[ bar]"])); + + await timeout(10000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(1,4)", triggerKind: 0, text: "foo" }])); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), ([])); + + await ctx.model.triggerExplicitly(); // reset to provider truth + await timeout(10000); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["foo[ baz]"])); + } + ); + }); + + test('when accepting undo', async function () { + // The user types the text as suggested and the provider reports a different suggestion. + + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async (ctx) => { + await setupScenario(ctx, provider); + + await ctx.model.acceptNextWord(); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["foo[ bar]"])); + + await timeout(10000); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), ([])); + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(1,4)", triggerKind: 0, text: "foo" }])); + + await ctx.editor.getModel().undo(); + await timeout(10000); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["f[oo bar]"])); + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(1,2)", triggerKind: 0, text: "f" }])); + + await ctx.editor.getModel().redo(); + await timeout(10000); + assert.deepStrictEqual(ctx.context.getAndClearViewStates(), (["foo[ bar]"])); + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([{ position: "(1,4)", triggerKind: 0, text: "foo" }])); + } + ); + }); + + test('Support backward instability', async function () { + // The user deletes text and the suggestion changes + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('fooba'); + + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 6) }); + + model.triggerExplicitly(); + await timeout(1000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,6)', text: 'fooba', triggerKind: 1, } + ]); + assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'fooba[r]']); + + provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 5) }); + context.leftDelete(); + await timeout(1000); + assert.deepStrictEqual(provider.getAndClearCallHistory(), [ + { position: '(1,5)', text: 'foob', triggerKind: 0, } + ]); + assert.deepStrictEqual(context.getAndClearViewStates(), [ + 'foob[ar]', + 'foob[az]' + ]); + } + ); + }); }); test('No race conditions', async function () { @@ -563,248 +523,199 @@ suite('Inline Completions', () => { } ); }); +}); - suite('inlineCompletionMultiCursor', () => { - - test('Basic', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('console\nconsole\n'); - editor.setSelections([ - new Selection(1, 1000, 1, 1000), - new Selection(2, 1000, 2, 1000), - ]); - provider.setReturnValue({ - insertText: 'console.log("hello");', - range: new Range(1, 1, 1, 1000), - }); - model.triggerExplicitly(); - await timeout(1000); - model.accept(editor); - assert.deepStrictEqual( - editor.getValue(), - [ - `console.log("hello");`, - `console.log("hello");`, - `` - ].join('\n') - ); - } - ); - }); +suite('Multi Cursor Support', () => { + ensureNoDisposablesAreLeakedInTestSuite(); - test('Multi Part', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('console.log()\nconsole.log\n'); - editor.setSelections([ - new Selection(1, 12, 1, 12), - new Selection(2, 1000, 2, 1000), - ]); - provider.setReturnValue({ - insertText: 'console.log("hello");', - range: new Range(1, 1, 1, 1000), - }); - model.triggerExplicitly(); - await timeout(1000); - model.accept(editor); - assert.deepStrictEqual( - editor.getValue(), - [ - `console.log("hello");`, - `console.log("hello");`, - `` - ].join('\n') - ); - } - ); - }); + test('Basic', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('console\nconsole\n'); + editor.setSelections([ + new Selection(1, 1000, 1, 1000), + new Selection(2, 1000, 2, 1000), + ]); + provider.setReturnValue({ + insertText: 'console.log("hello");', + range: new Range(1, 1, 1, 1000), + }); + model.triggerExplicitly(); + await timeout(1000); + model.accept(editor); + assert.deepStrictEqual( + editor.getValue(), + [ + `console.log("hello");`, + `console.log("hello");`, + `` + ].join('\n') + ); + } + ); + }); - test('Multi Part and Different Cursor Columns', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('console.log()\nconsole.warn\n'); - editor.setSelections([ - new Selection(1, 12, 1, 12), - new Selection(2, 14, 2, 14), - ]); - provider.setReturnValue({ - insertText: 'console.log("hello");', - range: new Range(1, 1, 1, 1000), - }); - model.triggerExplicitly(); - await timeout(1000); - model.accept(editor); - assert.deepStrictEqual( - editor.getValue(), - [ - `console.log("hello");`, - `console.warn("hello");`, - `` - ].join('\n') - ); - } - ); - }); + test('Multi Part', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('console.log()\nconsole.log\n'); + editor.setSelections([ + new Selection(1, 12, 1, 12), + new Selection(2, 1000, 2, 1000), + ]); + provider.setReturnValue({ + insertText: 'console.log("hello");', + range: new Range(1, 1, 1, 1000), + }); + model.triggerExplicitly(); + await timeout(1000); + model.accept(editor); + assert.deepStrictEqual( + editor.getValue(), + [ + `console.log("hello");`, + `console.log("hello");`, + `` + ].join('\n') + ); + } + ); + }); - async function acceptNextWord(model: InlineCompletionsModel, editor: ITestCodeEditor, timesToAccept: number = 1): Promise { - for (let i = 0; i < timesToAccept; i++) { + test('Multi Part and Different Cursor Columns', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('console.log()\nconsole.warn\n'); + editor.setSelections([ + new Selection(1, 12, 1, 12), + new Selection(2, 14, 2, 14), + ]); + provider.setReturnValue({ + insertText: 'console.log("hello");', + range: new Range(1, 1, 1, 1000), + }); model.triggerExplicitly(); await timeout(1000); - await model.acceptNextWord(editor); + model.accept(editor); + assert.deepStrictEqual( + editor.getValue(), + [ + `console.log("hello");`, + `console.warn("hello");`, + `` + ].join('\n') + ); } - } + ); + }); - test('Basic Partial Completion', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('let\nlet\n'); - editor.setSelections([ - new Selection(1, 1000, 1, 1000), - new Selection(2, 1000, 2, 1000), - ]); + async function acceptNextWord(model: InlineCompletionsModel, editor: ITestCodeEditor, timesToAccept: number = 1): Promise { + for (let i = 0; i < timesToAccept; i++) { + model.triggerExplicitly(); + await timeout(1000); + await model.acceptNextWord(); + } + } - provider.setReturnValue({ - insertText: `let a = 'some word'; `, - range: new Range(1, 1, 1, 1000), - }); - - await acceptNextWord(model, editor, 2); - - assert.deepStrictEqual( - editor.getValue(), - [ - `let a`, - `let a`, - `` - ].join('\n') - ); - } - ); - }); + test('Basic Partial Completion', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('let\nlet\n'); + editor.setSelections([ + new Selection(1, 1000, 1, 1000), + new Selection(2, 1000, 2, 1000), + ]); - test('Partial Multi-Part Completion', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType('for ()\nfor \n'); - editor.setSelections([ - new Selection(1, 5, 1, 5), - new Selection(2, 1000, 2, 1000), - ]); + provider.setReturnValue({ + insertText: `let a = 'some word'; `, + range: new Range(1, 1, 1, 1000), + }); - provider.setReturnValue({ - insertText: `for (let i = 0; i < 10; i++) {`, - range: new Range(1, 1, 1, 1000), - }); + await acceptNextWord(model, editor, 2); - model.triggerExplicitly(); - await timeout(1000); + assert.deepStrictEqual( + editor.getValue(), + [ + `let a`, + `let a`, + `` + ].join('\n') + ); + } + ); + }); - await acceptNextWord(model, editor, 3); + test('Partial Multi-Part Completion', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('for ()\nfor \n'); + editor.setSelections([ + new Selection(1, 5, 1, 5), + new Selection(2, 1000, 2, 1000), + ]); - assert.deepStrictEqual( - editor.getValue(), - [ - `for (let i)`, - `for (let i`, - `` - ].join('\n') - ); - } - ); - }); + provider.setReturnValue({ + insertText: `for (let i = 0; i < 10; i++) {`, + range: new Range(1, 1, 1, 1000), + }); - test('Partial Mutli-Part and Different Cursor Columns Completion', async function () { - const provider = new MockInlineCompletionsProvider(); - await withAsyncTestCodeEditorAndInlineCompletionsModel('', - { fakeClock: true, provider }, - async ({ editor, editorViewModel, model, context }) => { - context.keyboardType(`console.log()\nconsole.warnnnn\n`); - editor.setSelections([ - new Selection(1, 12, 1, 12), - new Selection(2, 16, 2, 16), - ]); + model.triggerExplicitly(); + await timeout(1000); - provider.setReturnValue({ - insertText: `console.log("hello" + " " + "world");`, - range: new Range(1, 1, 1, 1000), - }); + await acceptNextWord(model, editor, 3); - model.triggerExplicitly(); - await timeout(1000); + assert.deepStrictEqual( + editor.getValue(), + [ + `for (let i)`, + `for (let i`, + `` + ].join('\n') + ); + } + ); + }); - await acceptNextWord(model, editor, 4); + test('Partial Mutli-Part and Different Cursor Columns Completion', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType(`console.log()\nconsole.warnnnn\n`); + editor.setSelections([ + new Selection(1, 12, 1, 12), + new Selection(2, 16, 2, 16), + ]); - assert.deepStrictEqual( - editor.getValue(), - [ - `console.log("hello" + )`, - `console.warnnnn("hello" + `, - `` - ].join('\n') - ); - } - ); - }); - }); -}); + provider.setReturnValue({ + insertText: `console.log("hello" + " " + "world");`, + range: new Range(1, 1, 1, 1000), + }); -async function withAsyncTestCodeEditorAndInlineCompletionsModel( - text: string, - options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean }, - callback: (args: { editor: ITestCodeEditor; editorViewModel: ViewModel; model: InlineCompletionsModel; context: GhostTextContext }) => Promise -): Promise { - return await runWithFakedTimers({ - useFakeTimers: options.fakeClock, - }, async () => { - const disposableStore = new DisposableStore(); - - try { - if (options.provider) { - const languageFeaturesService = new LanguageFeaturesService(); - if (!options.serviceCollection) { - options.serviceCollection = new ServiceCollection(); - } - options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService); - options.serviceCollection.set(IAccessibilitySignalService, { - playSignal: async () => { }, - isSoundEnabled(signal: unknown) { return false; }, - } as any); - const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider); - disposableStore.add(d); - } + model.triggerExplicitly(); + await timeout(1000); - let result: T; - await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { - const controller = instantiationService.createInstance(InlineCompletionsController, editor); - const model = controller.model.get()!; - const context = new GhostTextContext(model, editor); - try { - result = await callback({ editor, editorViewModel, model, context }); - } finally { - context.dispose(); - model.dispose(); - controller.dispose(); - } - }); + await acceptNextWord(model, editor, 4); - if (options.provider instanceof MockInlineCompletionsProvider) { - options.provider.assertNotCalledTwiceWithin50ms(); + assert.deepStrictEqual( + editor.getValue(), + [ + `console.log("hello" + )`, + `console.warnnnn("hello" + `, + `` + ].join('\n') + ); } - - return result!; - } finally { - disposableStore.dispose(); - } + ); }); -} +}); diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts new file mode 100644 index 00000000000..ea8896ee649 --- /dev/null +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { timeout } from '../../../../../base/common/async.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { AnnotatedText, InlineEditContext, IWithAsyncTestCodeEditorAndInlineCompletionsModel, MockSearchReplaceCompletionsProvider, withAsyncTestCodeEditorAndInlineCompletionsModel } from './utils.js'; + +suite('Inline Edits', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const val = new AnnotatedText(` +class Point { + constructor(public x: number, public y: number) {} + + getLength2D(): number { + return↓ Math.sqrt(this.x * this.x + this.y * this.y↓); + } +} +`); + + async function runTest(cb: (ctx: IWithAsyncTestCodeEditorAndInlineCompletionsModel, provider: MockSearchReplaceCompletionsProvider, view: InlineEditContext) => Promise): Promise { + const provider = new MockSearchReplaceCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel(val.value, + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async (ctx) => { + const view = new InlineEditContext(ctx.model, ctx.editor); + ctx.store.add(view); + await cb(ctx, provider, view); + } + ); + } + + test('Can Accept Inline Edit', async function () { + await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => { + provider.add(`getLength2D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + }`, `getLength3D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + }`); + + await model.trigger(); + await timeout(10000); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + undefined, + "\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n" + ])); + + model.accept(); + + assert.deepStrictEqual(editor.getValue(), ` +class Point { + constructor(public x: number, public y: number) {} + + getLength3D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + } +} +`); + }); + }); + + test('Can Type Inline Edit', async function () { + await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => { + provider.add(`getLength2D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + }`, `getLength3D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + }`); + await model.trigger(); + await timeout(10000); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + undefined, + "\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n" + ])); + + editor.setPosition(val.getMarkerPosition(1)); + editorViewModel.type(' + t'); + + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + "\n\tget❰Length2↦Length3❱D(): numbe...\n...this.y + t❰his.z...his.z❱);\n" + ])); + + editorViewModel.type('his.z * this.z'); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + "\n\tget❰Length2↦Length3❱D(): numbe..." + ])); + }); + }); + + test('Inline Edit Stays On Unrelated Edit', async function () { + await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => { + provider.add(`getLength2D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + }`, `getLength3D(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + }`); + await model.trigger(); + await timeout(10000); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + undefined, + "\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n" + ])); + + editor.setPosition(val.getMarkerPosition(0)); + editorViewModel.type('/* */'); + + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + "\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n" + ])); + + await timeout(10000); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + undefined + ])); + }); + }); +}); diff --git a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 9cd0d114694..ffbb9897aa6 100644 --- a/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/code/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -5,14 +5,25 @@ import { timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js'; import { Position } from '../../../../common/core/position.js'; import { ITextModel } from '../../../../common/model.js'; -import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } from '../../../../common/languages.js'; -import { ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; +import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js'; +import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js'; -import { autorun } from '../../../../../base/common/observable.js'; +import { autorun, derived } from '../../../../../base/common/observable.js'; +import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; +import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; +import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js'; +import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js'; +import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js'; +import { Range } from '../../../../common/core/range.js'; +import { TextEdit } from '../../../../common/core/textEdit.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; @@ -58,7 +69,13 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider text: model.getValue() }); const result = new Array(); - result.push(...this.returnValue); + for (const v of this.returnValue) { + const x = { ...v }; + if (!x.range) { + x.range = model.getFullModelRange(); + } + result.push(x); + } if (this.delayMs > 0) { await timeout(this.delayMs); @@ -70,6 +87,66 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider handleItemDidShow() { } } +export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider { + private _map = new Map(); + + public add(search: string, replace: string): void { + this._map.set(search, replace); + } + + async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise { + const text = model.getValue(); + for (const [search, replace] of this._map) { + const idx = text.indexOf(search); + // replace idx...idx+text.length with replace + if (idx !== -1) { + const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length)); + return { + items: [ + { range, insertText: replace, isInlineEdit: true } + ] + }; + } + } + return { items: [] }; + } + freeInlineCompletions() { } + handleItemDidShow() { } +} + +export class InlineEditContext extends Disposable { + public readonly prettyViewStates = new Array(); + + constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) { + super(); + + const edit = derived(reader => { + const state = model.state.read(reader); + return state ? new TextEdit(state.edits) : undefined; + }); + + this._register(autorun(reader => { + /** @description update */ + const e = edit.read(reader); + let view: string | undefined; + + if (e) { + view = e.toString(this.editor.getValue()); + } else { + view = undefined; + } + + this.prettyViewStates.push(view); + })); + } + + public getAndClearViewStates(): (string | undefined)[] { + const arr = [...this.prettyViewStates]; + this.prettyViewStates.length = 0; + return arr; + } +} + export class GhostTextContext extends Disposable { public readonly prettyViewStates = new Array(); private _currentPrettyViewState: string | undefined; @@ -132,3 +209,116 @@ export class GhostTextContext extends Disposable { } } +export interface IWithAsyncTestCodeEditorAndInlineCompletionsModel { + editor: ITestCodeEditor; + editorViewModel: ViewModel; + model: InlineCompletionsModel; + context: GhostTextContext; + store: DisposableStore; +} + +export async function withAsyncTestCodeEditorAndInlineCompletionsModel( + text: string, + options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean }, + callback: (args: IWithAsyncTestCodeEditorAndInlineCompletionsModel) => Promise): Promise { + return await runWithFakedTimers({ + useFakeTimers: options.fakeClock, + }, async () => { + const disposableStore = new DisposableStore(); + + try { + if (options.provider) { + const languageFeaturesService = new LanguageFeaturesService(); + if (!options.serviceCollection) { + options.serviceCollection = new ServiceCollection(); + } + options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService); + options.serviceCollection.set(IAccessibilitySignalService, { + playSignal: async () => { }, + isSoundEnabled(signal: unknown) { return false; }, + } as any); + const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider); + disposableStore.add(d); + } + + let result: T; + await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { + const controller = instantiationService.createInstance(InlineCompletionsController, editor); + controller.testOnlyDisableUi(); + const model = controller.model.get()!; + const context = new GhostTextContext(model, editor); + try { + result = await callback({ editor, editorViewModel, model, context, store: disposableStore }); + } finally { + context.dispose(); + model.dispose(); + controller.dispose(); + } + }); + + if (options.provider instanceof MockInlineCompletionsProvider) { + options.provider.assertNotCalledTwiceWithin50ms(); + } + + return result!; + } finally { + disposableStore.dispose(); + } + }); +} + +export class AnnotatedString { + public readonly value: string; + public readonly markers: { mark: string; idx: number }[]; + + constructor(src: string, annotations: string[] = ['↓']) { + const markers = findMarkers(src, annotations); + this.value = markers.textWithoutMarkers; + this.markers = markers.results; + } + + getMarkerOffset(markerIdx = 0): number { + if (markerIdx >= this.markers.length) { + throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`); + } + return this.markers[markerIdx].idx; + } +} + +function findMarkers(text: string, markers: string[]): { + results: { mark: string; idx: number }[]; + textWithoutMarkers: string; +} { + const results: { mark: string; idx: number }[] = []; + let textWithoutMarkers = ''; + + markers.sort((a, b) => b.length - a.length); + + let pos = 0; + for (let i = 0; i < text.length;) { + let foundMarker = false; + for (const marker of markers) { + if (text.startsWith(marker, i)) { + results.push({ mark: marker, idx: pos }); + i += marker.length; + foundMarker = true; + break; + } + } + if (!foundMarker) { + textWithoutMarkers += text[i]; + pos++; + i++; + } + } + + return { results, textWithoutMarkers }; +} + +export class AnnotatedText extends AnnotatedString { + private readonly _transformer = new PositionOffsetTransformer(this.value); + + getMarkerPosition(markerIdx = 0): Position { + return this._transformer.getPosition(this.getMarkerOffset(markerIdx)); + } +} diff --git a/code/src/vs/editor/contrib/rename/browser/rename.ts b/code/src/vs/editor/contrib/rename/browser/rename.ts index 2165ff1e350..4fda88fd913 100644 --- a/code/src/vs/editor/contrib/rename/browser/rename.ts +++ b/code/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/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 0c4c59ff27e..9f443e037d6 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -96,6 +96,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._widgetState = StickyScrollWidgetState.Empty; const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); + this._register(this._editor.onDidChangeLineHeight((e) => { + e.changes.forEach((change) => { + const lineNumber = change.lineNumber; + if (this._widgetState.startLineNumbers.includes(lineNumber)) { + this._renderStickyScroll(lineNumber); + } + }); + })); this._register(this._editor.onDidChangeConfiguration(e => { this._readConfigurationChange(e); })); diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts index b313d5490df..7b0800a58c4 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.ts @@ -316,7 +316,7 @@ abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandid constructor(editor: IActiveCodeEditor) { super(editor); - this._foldingLimitReporter = new RangesLimitReporter(editor); + this._foldingLimitReporter = this._register(new RangesLimitReporter(editor)); } protected createStickyModel(token: CancellationToken, model: FoldingRegions): StickyModel { diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 4fbe34be92f..24f9a4c279d 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -162,7 +162,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi } } const lowerBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber, (a: number, b: number) => { return a - b; })); - const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber + depth, (a: number, b: number) => { return a - b; })); + const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.endLineNumber, (a: number, b: number) => { return a - b; })); for (let i = lowerBound; i <= upperBound; i++) { const child = outlineModel.children[i]; @@ -175,7 +175,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi const childEndLine = childRange.endLineNumber; if (range.startLineNumber <= childEndLine + 1 && childStartLine - 1 <= range.endLineNumber && childStartLine !== lastLine) { lastLine = childStartLine; - const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const lineHeight = this._editor.getLineHeightForLineNumber(childStartLine); result.push(new StickyLineCandidate(childStartLine, childEndLine - 1, top, lineHeight)); this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, childStartLine); } diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 9ec68fb92a8..ddda2843eb2 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -160,7 +160,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { if (!state) { return true; } - const futureWidgetHeight = state.startLineNumbers.length * this._lineHeight + state.lastLineRelativePosition; + const futureWidgetHeight = this._getHeightOfLines(state.startLineNumbers, state.lastLineRelativePosition); if (futureWidgetHeight > 0) { this._lastLineRelativePosition = state.lastLineRelativePosition; const lineNumbers = [...state.startLineNumbers]; @@ -228,18 +228,21 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._setHeight(0); return; } + let top: number = 0; // For existing sticky lines update the top and z-index for (const stickyLine of this._renderedStickyLines) { - this._updatePosition(stickyLine); + this._updatePosition(stickyLine, top); + top += stickyLine.height; } // For new sticky lines const layoutInfo = this._editor.getLayoutInfo(); const linesToRender = this._lineNumbers.slice(rebuildFromLine); for (const [index, line] of linesToRender.entries()) { - const stickyLine = this._renderChildNode(index + rebuildFromLine, line, foldingModel, layoutInfo); + const stickyLine = this._renderChildNode(index + rebuildFromLine, line, top, foldingModel, layoutInfo); if (!stickyLine) { continue; } + top += stickyLine.height; this._linesDomNode.appendChild(stickyLine.lineDomNode); this._lineNumbersDomNode.appendChild(stickyLine.lineNumberDomNode); this._renderedStickyLines.push(stickyLine); @@ -249,7 +252,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._useFoldingOpacityTransition(!this._isOnGlyphMargin); } - const widgetHeight = this._lineNumbers.length * this._lineHeight + this._lastLineRelativePosition; + const widgetHeight = top + this._lastLineRelativePosition; this._setHeight(widgetHeight); this._rootDomNode.style.marginLeft = '0px'; @@ -257,6 +260,14 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._editor.layoutOverlayWidget(this); } + private _getHeightOfLines(lineNumbers: number[], lastLineRelativePosition: number): number { + let totalHeight = 0; + for (let i = 0; i < lineNumbers.length; i++) { + totalHeight += this._editor.getLineHeightForLineNumber(lineNumbers[i]); + } + return totalHeight + lastLineRelativePosition; + } + private _setHeight(height: number): void { if (this._height === height) { return; @@ -291,7 +302,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, top: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -307,7 +318,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { actualInlineDecorations = []; } - const lineHeight = this._lineHeight; + const lineHeight = this._editor.getLineHeightForLineNumber(line); const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, lineRenderingData.continuesWithWrappedLine, lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, @@ -359,6 +370,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { if (foldingIcon) { lineNumberHTMLNode.appendChild(foldingIcon.domNode); foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`; + foldingIcon.domNode.style.lineHeight = `${lineHeight}px`; } this._editor.applyFontInfo(lineHTMLNode); @@ -371,10 +383,10 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { lineHTMLNode.style.height = `${lineHeight}px`; const renderedLine = new RenderedStickyLine(index, line, lineHTMLNode, lineNumberHTMLNode, foldingIcon, renderOutput.characterMapping, lineHTMLNode.scrollWidth, lineHeight); - return this._updatePosition(renderedLine); + return this._updatePosition(renderedLine, top); } - private _updatePosition(stickyLine: RenderedStickyLine): RenderedStickyLine { + private _updatePosition(stickyLine: RenderedStickyLine, top: number): RenderedStickyLine { const index = stickyLine.index; const lineHTMLNode = stickyLine.lineDomNode; const lineNumberHTMLNode = stickyLine.lineNumberDomNode; @@ -383,16 +395,15 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const zIndex = '0'; lineHTMLNode.style.zIndex = zIndex; lineNumberHTMLNode.style.zIndex = zIndex; - const top = `${index * this._lineHeight + this._lastLineRelativePosition + (stickyLine.foldingIcon?.isCollapsed ? 1 : 0)}px`; - lineHTMLNode.style.top = top; - lineNumberHTMLNode.style.top = top; + const updatedTop = `${top + this._lastLineRelativePosition + (stickyLine.foldingIcon?.isCollapsed ? 1 : 0)}px`; + lineHTMLNode.style.top = updatedTop; + lineNumberHTMLNode.style.top = updatedTop; } else { const zIndex = '1'; lineHTMLNode.style.zIndex = zIndex; lineNumberHTMLNode.style.zIndex = zIndex; - const top = `${index * this._lineHeight}px`; - lineHTMLNode.style.top = top; - lineNumberHTMLNode.style.top = top; + lineHTMLNode.style.top = `${top}px`; + lineNumberHTMLNode.style.top = `${top}px`; } return stickyLine; } diff --git a/code/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/code/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 07493f90714..918e06d8aeb 100644 --- a/code/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/code/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -150,7 +150,7 @@ suite('Sticky Scroll Tests', () => { await provider.update(); assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 1, endLineNumber: 4 }), [new StickyLineCandidate(1, 2, 0, 19)]); assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 8, endLineNumber: 10 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19)]); - assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 10, endLineNumber: 13 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19)]); + assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 10, endLineNumber: 13 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19), new StickyLineCandidate(13, 13, 0, 19)]); provider.dispose(); model.dispose(); diff --git a/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index f22622e5ac3..71ae4e428fe 100644 --- a/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/code/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -36,6 +36,7 @@ import { ItemRenderer } from './suggestWidgetRenderer.js'; import { getListStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; import { CompletionItemKinds } from '../../../common/languages.js'; +import { isWindows } from '../../../../base/common/platform.js'; /** * Suggest widget colors @@ -231,7 +232,7 @@ export class SuggestWidget implements IDisposable { mouseSupport: false, multipleSelectionSupport: false, accessibilityProvider: { - getRole: () => 'listitem', + getRole: () => isWindows ? 'listitem' : 'option', getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"), getWidgetRole: () => 'listbox', getAriaLabel: (item: CompletionItem) => { diff --git a/code/src/vs/editor/contrib/unicodeHighlighter/browser/bannerController.ts b/code/src/vs/editor/contrib/unicodeHighlighter/browser/bannerController.ts index d96488677d9..e23a826de08 100644 --- a/code/src/vs/editor/contrib/unicodeHighlighter/browser/bannerController.ts +++ b/code/src/vs/editor/contrib/unicodeHighlighter/browser/bannerController.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import './bannerController.css'; +import { localize } from '../../../../nls.js'; import { $, append, clearNode } from '../../../../base/browser/dom.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Action } from '../../../../base/common/actions.js'; @@ -129,7 +130,7 @@ class Banner extends Disposable { this.actionBar.push(this._register( new Action( 'banner.close', - 'Close Banner', + localize('closeBanner', "Close Banner"), ThemeIcon.asClassName(widgetClose), true, () => { diff --git a/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 2a65960d983..96820f1eafd 100644 --- a/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/code/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -370,7 +370,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { } if (this.options.showFrame) { - const frameThickness = Math.round(lineHeight / 9); + const frameThickness = this.options.frameWidth ?? Math.round(lineHeight / 9); result += 2 * frameThickness; } diff --git a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 03a2821ce51..0b0da3e8e37 100644 --- a/code/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/code/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/code/src/vs/editor/standalone/browser/standaloneLanguages.ts b/code/src/vs/editor/standalone/browser/standaloneLanguages.ts index 849e4dc42d7..2dc93ca30c6 100644 --- a/code/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/code/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -812,6 +812,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { NewSymbolNameTriggerKind: standaloneEnums.NewSymbolNameTriggerKind, PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, HoverVerbosityAction: standaloneEnums.HoverVerbosityAction, + InlineCompletionEndOfLifeReasonKind: standaloneEnums.InlineCompletionEndOfLifeReasonKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/code/src/vs/editor/standalone/browser/standaloneServices.ts b/code/src/vs/editor/standalone/browser/standaloneServices.ts index ddf887c8b2b..c3208cb7b76 100644 --- a/code/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/code/src/vs/editor/standalone/browser/standaloneServices.ts @@ -42,7 +42,7 @@ import { IKeybindingItem, KeybindingsRegistry } from '../../../platform/keybindi import { ResolvedKeybindingItem } from '../../../platform/keybinding/common/resolvedKeybindingItem.js'; import { USLayoutResolvedKeybinding } from '../../../platform/keybinding/common/usLayoutResolvedKeybinding.js'; import { ILabelService, ResourceLabelFormatter, IFormatterChangeEvent, Verbosity } from '../../../platform/label/common/label.js'; -import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from '../../../platform/notification/common/notification.js'; +import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter, IStatusHandle } from '../../../platform/notification/common/notification.js'; import { IProgressRunner, IEditorProgressService, IProgressService, IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from '../../../platform/progress/common/progress.js'; import { ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, WorkbenchState, WorkspaceFolder, STANDALONE_EDITOR_WORKSPACE_ID } from '../../../platform/workspace/common/workspace.js'; @@ -350,11 +350,10 @@ export class StandaloneNotificationService implements INotificationService { return StandaloneNotificationService.NO_OP; } - public status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + public status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } - public setFilter(filter: NotificationsFilter | INotificationSourceFilter): void { } public getFilter(source?: INotificationSource): NotificationsFilter { diff --git a/code/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/code/src/vs/editor/test/browser/config/editorConfiguration.test.ts index 3dcf646314f..ce875cd7037 100644 --- a/code/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/code/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -66,7 +66,8 @@ suite('Common Editor Config', () => { outerHeight: 100, emptySelectionClipboard: true, pixelRatio: 1, - accessibilitySupport: AccessibilitySupport.Unknown + accessibilitySupport: AccessibilitySupport.Unknown, + editContextSupported: true, }; } } diff --git a/code/src/vs/editor/test/browser/config/testConfiguration.ts b/code/src/vs/editor/test/browser/config/testConfiguration.ts index 3c1862c967d..ece0400a78c 100644 --- a/code/src/vs/editor/test/browser/config/testConfiguration.ts +++ b/code/src/vs/editor/test/browser/config/testConfiguration.ts @@ -25,7 +25,8 @@ export class TestConfiguration extends EditorConfiguration { outerHeight: envConfig?.outerHeight ?? 100, emptySelectionClipboard: envConfig?.emptySelectionClipboard ?? true, pixelRatio: envConfig?.pixelRatio ?? 1, - accessibilitySupport: envConfig?.accessibilitySupport ?? AccessibilitySupport.Unknown + accessibilitySupport: envConfig?.accessibilitySupport ?? AccessibilitySupport.Unknown, + editContextSupported: true }; } diff --git a/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts b/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts index 5ebb74c6537..a85a5c62bea 100644 --- a/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts +++ b/code/src/vs/editor/test/browser/services/treeSitterParserService.test.ts @@ -14,6 +14,7 @@ import { mock } from '../../../../base/test/common/mock.js'; import { ITreeSitterImporter } from '../../../common/services/treeSitterParserService.js'; import { TextModelTreeSitter } from '../../../common/services/treeSitter/textModelTreeSitter.js'; import { TreeSitterLanguages } from '../../../common/services/treeSitter/treeSitterLanguages.js'; +import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js'; class MockParser implements Parser.Parser { language: Parser.Language | null = null; @@ -168,7 +169,8 @@ suite('TreeSitterParserService', function () { } } - const treeSitterLanguages: TreeSitterLanguages = store.add(new MockTreeSitterLanguages(treeSitterImporter, {} as any, { isBuilt: false } as any, new Map())); + const mockConfigurationService = new TestConfigurationService(); + const treeSitterLanguages: TreeSitterLanguages = store.add(new MockTreeSitterLanguages(treeSitterImporter, {} as any, { isBuilt: false } as any, mockConfigurationService, new Map())); const textModel = store.add(createTextModel('console.log("Hello, world!");', 'javascript')); const textModelTreeSitter = store.add(new TextModelTreeSitter(textModel, treeSitterLanguages, false, treeSitterImporter, logService, telemetryService, { exists: async () => false } as any)); textModel.setLanguage('typescript'); diff --git a/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts b/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts index 7bb14f13871..6596c70b875 100644 --- a/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts +++ b/code/src/vs/editor/test/browser/widget/observableCodeEditor.test.ts @@ -37,11 +37,13 @@ suite("CodeEditorWidget", () => { const derived = derivedHandleChanges( { - createEmptyChangeSummary: () => undefined, - handleChange: (context) => { - const obsName = observableName(context.changedObservable, obsEditor); - log.log(`handle change: ${obsName} ${formatChange(context.change)}`); - return true; + changeTracker: { + createChangeSummary: () => undefined, + handleChange: (context) => { + const obsName = observableName(context.changedObservable, obsEditor); + log.log(`handle change: ${obsName} ${formatChange(context.change)}`); + return true; + }, }, }, (reader) => { diff --git a/code/src/vs/editor/test/common/codecs/baseToken.test.ts b/code/src/vs/editor/test/common/codecs/baseToken.test.ts new file mode 100644 index 00000000000..284e0e24732 --- /dev/null +++ b/code/src/vs/editor/test/common/codecs/baseToken.test.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { Range } from '../../../common/core/range.js'; +import { randomInt } from '../../../../base/common/numbers.js'; +import { BaseToken } from '../../../common/codecs/baseToken.js'; +import { assertDefined } from '../../../../base/common/types.js'; +import { randomBoolean } from '../../../../base/test/common/testUtils.js'; +import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TSimpleToken, WELL_KNOWN_TOKENS } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { ISimpleTokenClass, SimpleToken } from '../../../common/codecs/simpleCodec/tokens/simpleToken.js'; +import { At, Colon, DollarSign, ExclamationMark, Hash, LeftAngleBracket, LeftBracket, LeftCurlyBrace, RightAngleBracket, RightBracket, RightCurlyBrace, Slash, Space, Word } from '../../../common/codecs/simpleCodec/tokens/index.js'; + +/** + * Generates a random {@link Range} object. + * + * @throws if {@link maxNumber} argument is less than `2`, + * is equal to `NaN` or is `infinite`. + */ +const randomRange = ( + maxNumber: number = 1_000, +): Range => { + assert( + maxNumber > 1, + `Max number must be greater than 1, got '${maxNumber}'.`, + ); + + const startLineNumber = randomInt(maxNumber, 1); + const endLineNumber = (randomBoolean() === true) + ? startLineNumber + : randomInt(2 * maxNumber, startLineNumber); + + const startColumnNumber = randomInt(maxNumber, 1); + const endColumnNumber = (randomBoolean() === true) + ? startColumnNumber + 1 + : randomInt(2 * maxNumber, startColumnNumber + 1); + + return new Range( + startLineNumber, + startColumnNumber, + endLineNumber, + endColumnNumber, + ); +}; + +/** + * List of simple tokens to randomly select from + * in the {@link randomSimpleToken} utility. + */ +const TOKENS: readonly ISimpleTokenClass[] = Object.freeze([ + ...WELL_KNOWN_TOKENS, + CarriageReturn, + NewLine, +]); + +/** + * Generates a random {@link SimpleToken} instance. + */ +const randomSimpleToken = (): TSimpleToken => { + const index = randomInt(TOKENS.length - 1); + + const Constructor = TOKENS[index]; + assertDefined( + Constructor, + `Cannot find a constructor object for a well-known token at index '${index}'.`, + ); + + return new Constructor(randomRange()); +}; + +suite('BaseToken', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('• render', () => { + /** + * Note! Range of tokens is ignored by the render method, hence + * we generate random ranges for each token in this test. + */ + test('• a list of tokens', () => { + const tests: readonly [string, BaseToken[]][] = [ + ['/textoftheword$#', [ + new Slash(randomRange()), + new Word(randomRange(), 'textoftheword'), + new DollarSign(randomRange()), + new Hash(randomRange()), + ]], + ['<:👋helou👋:>', [ + new LeftAngleBracket(randomRange()), + new Colon(randomRange()), + new Word(randomRange(), '👋helou👋'), + new Colon(randomRange()), + new RightAngleBracket(randomRange()), + ]], + [' {$#[ !@! ]#$} ', [ + new Space(randomRange()), + new LeftCurlyBrace(randomRange()), + new DollarSign(randomRange()), + new Hash(randomRange()), + new LeftBracket(randomRange()), + new Space(randomRange()), + new ExclamationMark(randomRange()), + new At(randomRange()), + new ExclamationMark(randomRange()), + new Space(randomRange()), + new RightBracket(randomRange()), + new Hash(randomRange()), + new DollarSign(randomRange()), + new RightCurlyBrace(randomRange()), + new Space(randomRange()), + ]], + ]; + + for (const test of tests) { + const [expectedText, tokens] = test; + + assert.strictEqual( + expectedText, + BaseToken.render(tokens), + ); + } + }); + + test('• an empty list of tokens', () => { + assert.strictEqual( + '', + BaseToken.render([]), + `Must correctly render and empty list of tokens.`, + ); + }); + }); + + suite('• fullRange', () => { + suite('• throws', () => { + test('• if empty list provided', () => { + assert.throws(() => { + BaseToken.fullRange([]); + }); + }); + + test('• if start line number of the first token is greater than one of the last token', () => { + assert.throws(() => { + const lastToken = randomSimpleToken(); + + // generate a first token with starting line number that is + // greater than the start line number of the last token + const startLineNumber = lastToken.range.startLineNumber + randomInt(10, 1); + const firstToken = new Colon( + new Range( + startLineNumber, + lastToken.range.startColumn, + startLineNumber, + lastToken.range.startColumn + 1, + ), + ); + + BaseToken.fullRange([ + firstToken, + // tokens in the middle are ignored, so we + // generate random ones to fill the gap + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + // - + lastToken, + ]); + }); + }); + + test('• if start line numbers are equal and end of the first token is greater than the start of the last token', () => { + assert.throws(() => { + const firstToken = randomSimpleToken(); + + const lastToken = new Hash( + new Range( + firstToken.range.startLineNumber, + firstToken.range.endColumn - 1, + firstToken.range.startLineNumber + randomInt(10), + firstToken.range.endColumn, + ), + ); + + BaseToken.fullRange([ + firstToken, + // tokens in the middle are ignored, so we + // generate random ones to fill the gap + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + randomSimpleToken(), + // - + lastToken, + ]); + }); + }); + }); + }); +}); diff --git a/code/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts b/code/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts new file mode 100644 index 00000000000..cc2e893d95a --- /dev/null +++ b/code/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../common/core/range.js'; +import { TestDecoder } from '../utils/testDecoder.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { newWriteableStream } from '../../../../base/common/stream.js'; +import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { DoubleQuote } from '../../../common/codecs/simpleCodec/tokens/doubleQuote.js'; +import { type TSimpleDecoderToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { FrontMatterDecoder } from '../../../common/codecs/frontMatterCodec/frontMatterDecoder.js'; +import { ExclamationMark, Quote, Tab, Word, Space, Colon } from '../../../common/codecs/simpleCodec/tokens/index.js'; +import { FrontMatterBoolean, FrontMatterString, FrontMatterArray, FrontMatterRecord, FrontMatterRecordDelimiter, FrontMatterRecordName } from '../../../common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Front Matter decoder for testing purposes. + */ +export class TestFrontMatterDecoder extends TestDecoder { + constructor() { + const stream = newWriteableStream(null); + const decoder = new FrontMatterDecoder(stream); + + super(stream, decoder); + } +} + +suite('FrontMatterDecoder', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('• produces expected tokens', async () => { + const test = disposables.add( + new TestFrontMatterDecoder(), + ); + + await test.run( + [ + 'just: "write some yaml "', + 'write-some :\t[ \' just\t \', "yaml!", true, , ,]', + 'anotherField \t\t\t : FALSE ', + ], + [ + // first record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(1, 1, 1, 1 + 4), 'just'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(1, 5, 1, 6)), + new Space(new Range(1, 6, 1, 7)), + ]), + new FrontMatterString([ + new DoubleQuote(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 8 + 5), 'write'), + new Space(new Range(1, 13, 1, 14)), + new Word(new Range(1, 14, 1, 14 + 4), 'some'), + new Space(new Range(1, 18, 1, 19)), + new Word(new Range(1, 19, 1, 19 + 4), 'yaml'), + new Space(new Range(1, 23, 1, 24)), + new DoubleQuote(new Range(1, 24, 1, 25)), + ]), + ]), + new NewLine(new Range(1, 25, 1, 26)), + // second record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(2, 1, 2, 1 + 10), 'write-some'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(2, 12, 2, 13)), + new Tab(new Range(2, 13, 2, 14)), + ]), + new FrontMatterArray([ + new LeftBracket(new Range(2, 14, 2, 15)), + new FrontMatterString([ + new Quote(new Range(2, 16, 2, 17)), + new Space(new Range(2, 17, 2, 18)), + new Word(new Range(2, 18, 2, 18 + 4), 'just'), + new Tab(new Range(2, 22, 2, 23)), + new Space(new Range(2, 23, 2, 24)), + new Quote(new Range(2, 24, 2, 25)), + ]), + new FrontMatterString([ + new DoubleQuote(new Range(2, 28, 2, 29)), + new Word(new Range(2, 29, 2, 29 + 4), 'yaml'), + new ExclamationMark(new Range(2, 33, 2, 34)), + new DoubleQuote(new Range(2, 34, 2, 35)), + ]), + new FrontMatterBoolean( + new Range(2, 37, 2, 37 + 4), + true, + ), + new RightBracket(new Range(2, 46, 2, 47)), + ]), + ]), + new NewLine(new Range(2, 47, 2, 48)), + // third record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(3, 1, 3, 1 + 12), 'anotherField'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(3, 19, 3, 20)), + new Space(new Range(3, 20, 3, 21)), + ]), + new FrontMatterBoolean( + new Range(3, 22, 3, 22 + 5), + false, + ), + ]), + new Space(new Range(3, 27, 3, 28)), + ]); + }); +}); diff --git a/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts b/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts index 019344128bc..82ff987120a 100644 --- a/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts +++ b/code/src/vs/editor/test/common/codecs/linesDecoder.test.ts @@ -16,8 +16,8 @@ import { LinesDecoder, TLineToken } from '../../../common/codecs/linesCodec/line import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; /** - * Note! This decoder is also often used to test common logic of abstract {@linkcode BaseDecoder} - * class, because the {@linkcode LinesDecoder} is one of the simplest non-abstract decoders we have. + * Note! This decoder is also often used to test common logic of abstract {@link BaseDecoder} + * class, because the {@link LinesDecoder} is one of the simplest non-abstract decoders we have. */ suite('LinesDecoder', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -26,14 +26,14 @@ suite('LinesDecoder', () => { * Test the core logic with specific method of consuming * tokens that are produced by a lines decoder instance. */ - suite('core logic', () => { + suite('• core logic', () => { testLinesDecoder('async-generator', disposables); testLinesDecoder('consume-all-method', disposables); testLinesDecoder('on-data-event', disposables); }); - suite('settled promise', () => { - test('throws if accessed on not-yet-started decoder instance', () => { + suite('• settled promise', () => { + test('• throws if accessed on not-yet-started decoder instance', () => { const test = disposables.add(new TestLinesDecoder()); assert.throws( @@ -51,8 +51,8 @@ suite('LinesDecoder', () => { }); }); - suite('start', () => { - test('throws if the decoder object is already `disposed`', () => { + suite('• start', () => { + test('• throws if the decoder object is already `disposed`', () => { const test = disposables.add(new TestLinesDecoder()); const { decoder } = test; decoder.dispose(); @@ -63,7 +63,7 @@ suite('LinesDecoder', () => { ); }); - test('throws if the decoder object is already `ended`', async () => { + test('• throws if the decoder object is already `ended`', async () => { const inputStream = newWriteableStream(null); const test = disposables.add(new TestLinesDecoder(inputStream)); const { decoder } = test; @@ -129,8 +129,8 @@ export class TestLinesDecoder extends TestDecoder { } /** - * Common reusable test utility to validate {@linkcode LinesDecoder} logic with - * the provided {@linkcode tokensConsumeMethod} way of consuming decoder-produced tokens. + * Common reusable test utility to validate {@link LinesDecoder} logic with + * the provided {@link tokensConsumeMethod} way of consuming decoder-produced tokens. * * @throws if a test fails, please see thrown error for failure details. * @param tokensConsumeMethod The way to consume tokens produced by the decoder. @@ -141,8 +141,26 @@ function testLinesDecoder( disposables: Pick, ) { suite(tokensConsumeMethod, () => { - suite('produces expected tokens', () => { - test('input starts with line data', async () => { + suite('• produces expected tokens', () => { + test('• input starts with line data', async () => { + const test = disposables.add(new TestLinesDecoder()); + + await test.run( + ' hello world\nhow are you doing?\n\n 😊 \r', + [ + new Line(1, ' hello world'), + new NewLine(new Range(1, 13, 1, 14)), + new Line(2, 'how are you doing?'), + new NewLine(new Range(2, 19, 2, 20)), + new Line(3, ''), + new NewLine(new Range(3, 1, 3, 2)), + new Line(4, ' 😊 '), + new NewLine(new Range(4, 5, 4, 6)), + ], + ); + }); + + test('• standalone \\r is treated as new line', async () => { const test = disposables.add(new TestLinesDecoder()); await test.run( @@ -155,13 +173,13 @@ function testLinesDecoder( new Line(3, ''), new NewLine(new Range(3, 1, 3, 2)), new Line(4, ' 😊 '), - new CarriageReturn(new Range(4, 5, 4, 6)), + new NewLine(new Range(4, 5, 4, 6)), new Line(5, ' '), ], ); }); - test('input starts with a new line', async () => { + test('• input starts with a new line', async () => { const test = disposables.add(new TestLinesDecoder()); await test.run( @@ -184,7 +202,7 @@ function testLinesDecoder( ); }); - test('input starts and ends with multiple new lines', async () => { + test('• input starts and ends with multiple new lines', async () => { const test = disposables.add(new TestLinesDecoder()); await test.run( @@ -211,18 +229,18 @@ function testLinesDecoder( ); }); - test('single carriage return is treated as new line', async () => { + test('• single carriage return is treated as new line', async () => { const test = disposables.add(new TestLinesDecoder()); await test.run( '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', [ new Line(1, ''), - new CarriageReturn(new Range(1, 1, 1, 2)), + new NewLine(new Range(1, 1, 1, 2)), new Line(2, ''), - new CarriageReturn(new Range(2, 1, 2, 2)), + new NewLine(new Range(2, 1, 2, 2)), new Line(3, 'haalo! 💥💥 how\'re you?'), - new CarriageReturn(new Range(3, 24, 3, 25)), + new NewLine(new Range(3, 24, 3, 25)), new Line(4, ' ?!'), new CarriageReturn(new Range(4, 4, 4, 5)), new NewLine(new Range(4, 5, 4, 6)), diff --git a/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts b/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts index d0ea73d7fa2..2219068bef3 100644 --- a/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts +++ b/code/src/vs/editor/test/common/codecs/markdownDecoder.test.ts @@ -12,6 +12,7 @@ import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; import { Dash } from '../../../common/codecs/simpleCodec/tokens/dash.js'; import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; +import { Slash } from '../../../common/codecs/simpleCodec/tokens/slash.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalTab.js'; @@ -195,9 +196,15 @@ suite('MarkdownDecoder', () => { new Space(new Range(1, 2, 1, 3)), new RightBracket(new Range(1, 3, 1, 4)), new LeftParenthesis(new Range(1, 4, 1, 5)), - new Word(new Range(1, 5, 1, 5 + 11), './real/file'), + new Word(new Range(1, 5, 1, 5 + 1), '.'), + new Slash(new Range(1, 6, 1, 7)), + new Word(new Range(1, 7, 1, 7 + 4), 'real'), + new Slash(new Range(1, 11, 1, 12)), + new Word(new Range(1, 12, 1, 12 + 4), 'file'), new Space(new Range(1, 16, 1, 17)), - new Word(new Range(1, 17, 1, 17 + 17), 'path/file⇧name.md'), + new Word(new Range(1, 17, 1, 17 + 4), 'path'), + new Slash(new Range(1, 21, 1, 22)), + new Word(new Range(1, 22, 1, 22 + 12), 'file⇧name.md'), new NewLine(new Range(1, 34, 1, 35)), // `2nd` line new LeftBracket(new Range(2, 1, 2, 2)), @@ -207,22 +214,26 @@ suite('MarkdownDecoder', () => { new RightBracket(new Range(2, 11, 2, 12)), new Space(new Range(2, 12, 2, 13)), new LeftParenthesis(new Range(2, 13, 2, 14)), - new Word(new Range(2, 14, 2, 14 + 6), './file'), + new Word(new Range(2, 14, 2, 14 + 1), '.'), + new Slash(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 4), 'file'), new Space(new Range(2, 20, 2, 21)), - new Word(new Range(2, 21, 2, 21 + 13), 'path/name.txt'), + new Word(new Range(2, 21, 2, 21 + 4), 'path'), + new Slash(new Range(2, 25, 2, 26)), + new Word(new Range(2, 26, 2, 26 + 8), 'name.txt'), new RightParenthesis(new Range(2, 34, 2, 35)), ], ); }); suite('• stop characters inside caption/reference (new lines)', () => { - for (const stopCharacter of [CarriageReturn, NewLine]) { + for (const StopCharacter of [CarriageReturn, NewLine]) { let characterName = ''; - if (stopCharacter === CarriageReturn) { + if (StopCharacter === CarriageReturn) { characterName = '\\r'; } - if (stopCharacter === NewLine) { + if (StopCharacter === NewLine) { characterName = '\\n'; } @@ -238,11 +249,11 @@ suite('MarkdownDecoder', () => { const inputLines = [ // stop character inside link caption - `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + `[haa${StopCharacter.symbol}loů](./real/💁/name.txt)`, // stop character inside link reference - `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + `[ref text](/etc/pat${StopCharacter.symbol}h/to/file.md)`, // stop character between line caption and link reference is disallowed - `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + `[text]${StopCharacter.symbol}(/etc/ path/main.mdc)`, ]; @@ -252,11 +263,17 @@ suite('MarkdownDecoder', () => { // `1st` input line new LeftBracket(new Range(1, 1, 1, 2)), new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new NewLine(new Range(1, 5, 1, 6)), // a single CR token is treated as a `new line` new Word(new Range(2, 1, 2, 1 + 3), 'loů'), new RightBracket(new Range(2, 4, 2, 5)), new LeftParenthesis(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.txt'), + new Word(new Range(2, 6, 2, 6 + 1), '.'), + new Slash(new Range(2, 7, 2, 8)), + new Word(new Range(2, 8, 2, 8 + 4), 'real'), + new Slash(new Range(2, 12, 2, 13)), + new Word(new Range(2, 13, 2, 13 + 2), '💁'), + new Slash(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 8), 'name.txt'), new RightParenthesis(new Range(2, 24, 2, 25)), new NewLine(new Range(2, 25, 2, 26)), // `2nd` input line @@ -266,21 +283,32 @@ suite('MarkdownDecoder', () => { new Word(new Range(3, 6, 3, 6 + 4), 'text'), new RightBracket(new Range(3, 10, 3, 11)), new LeftParenthesis(new Range(3, 11, 3, 12)), - new Word(new Range(3, 12, 3, 12 + 8), '/etc/pat'), - new stopCharacter(new Range(3, 20, 3, 21)), // <- stop character - new Word(new Range(4, 1, 4, 1 + 12), 'h/to/file.md'), + new Slash(new Range(3, 12, 3, 13)), + new Word(new Range(3, 13, 3, 13 + 3), 'etc'), + new Slash(new Range(3, 16, 3, 17)), + new Word(new Range(3, 17, 3, 17 + 3), 'pat'), + new NewLine(new Range(3, 20, 3, 21)), // a single CR token is treated as a `new line` + new Word(new Range(4, 1, 4, 1 + 1), 'h'), + new Slash(new Range(4, 2, 4, 3)), + new Word(new Range(4, 3, 4, 3 + 2), 'to'), + new Slash(new Range(4, 5, 4, 6)), + new Word(new Range(4, 6, 4, 6 + 7), 'file.md'), new RightParenthesis(new Range(4, 13, 4, 14)), new NewLine(new Range(4, 14, 4, 15)), // `3nd` input line new LeftBracket(new Range(5, 1, 5, 2)), new Word(new Range(5, 2, 5, 2 + 4), 'text'), new RightBracket(new Range(5, 6, 5, 7)), - new stopCharacter(new Range(5, 7, 5, 8)), // <- stop character + new NewLine(new Range(5, 7, 5, 8)), // a single CR token is treated as a `new line` new LeftParenthesis(new Range(6, 1, 6, 2)), - new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Slash(new Range(6, 2, 6, 3)), + new Word(new Range(6, 3, 6, 3 + 3), 'etc'), + new Slash(new Range(6, 6, 6, 7)), new Space(new Range(6, 7, 6, 8)), - new Word(new Range(6, 8, 6, 8 + 12), 'path/file.md'), - new RightParenthesis(new Range(6, 20, 6, 21)), + new Word(new Range(6, 8, 6, 8 + 4), 'path'), + new Slash(new Range(6, 12, 6, 13)), + new Word(new Range(6, 13, 6, 13 + 8), 'main.mdc'), + new RightParenthesis(new Range(6, 21, 6, 22)), ], ); }); @@ -291,13 +319,13 @@ suite('MarkdownDecoder', () => { * Same as above but these stop characters do not move the caret to the next line. */ suite('• stop characters inside caption/reference (same line)', () => { - for (const stopCharacter of [VerticalTab, FormFeed]) { + for (const StopCharacter of [VerticalTab, FormFeed]) { let characterName = ''; - if (stopCharacter === VerticalTab) { + if (StopCharacter === VerticalTab) { characterName = '\\v'; } - if (stopCharacter === FormFeed) { + if (StopCharacter === FormFeed) { characterName = '\\f'; } @@ -313,11 +341,11 @@ suite('MarkdownDecoder', () => { const inputLines = [ // stop character inside link caption - `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + `[haa${StopCharacter.symbol}loů](./real/💁/name.txt)`, // stop character inside link reference - `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + `[ref text](/etc/pat${StopCharacter.symbol}h/to/file.md)`, // stop character between line caption and link reference is disallowed - `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + `[text]${StopCharacter.symbol}(/etc/ path/file.md)`, ]; @@ -327,11 +355,17 @@ suite('MarkdownDecoder', () => { // `1st` input line new LeftBracket(new Range(1, 1, 1, 2)), new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new StopCharacter(new Range(1, 5, 1, 6)), // <- stop character new Word(new Range(1, 6, 1, 6 + 3), 'loů'), new RightBracket(new Range(1, 9, 1, 10)), new LeftParenthesis(new Range(1, 10, 1, 11)), - new Word(new Range(1, 11, 1, 11 + 18), './real/💁/name.txt'), + new Word(new Range(1, 11, 1, 11 + 1), '.'), + new Slash(new Range(1, 12, 1, 13)), + new Word(new Range(1, 13, 1, 13 + 4), 'real'), + new Slash(new Range(1, 17, 1, 18)), + new Word(new Range(1, 18, 1, 18 + 2), '💁'), + new Slash(new Range(1, 20, 1, 21)), + new Word(new Range(1, 21, 1, 21 + 8), 'name.txt'), new RightParenthesis(new Range(1, 29, 1, 30)), new NewLine(new Range(1, 30, 1, 31)), // `2nd` input line @@ -341,20 +375,31 @@ suite('MarkdownDecoder', () => { new Word(new Range(2, 6, 2, 6 + 4), 'text'), new RightBracket(new Range(2, 10, 2, 11)), new LeftParenthesis(new Range(2, 11, 2, 12)), - new Word(new Range(2, 12, 2, 12 + 8), '/etc/pat'), - new stopCharacter(new Range(2, 20, 2, 21)), // <- stop character - new Word(new Range(2, 21, 2, 21 + 12), 'h/to/file.md'), + new Slash(new Range(2, 12, 2, 13)), + new Word(new Range(2, 13, 2, 13 + 3), 'etc'), + new Slash(new Range(2, 16, 2, 17)), + new Word(new Range(2, 17, 2, 17 + 3), 'pat'), + new StopCharacter(new Range(2, 20, 2, 21)), // <- stop character + new Word(new Range(2, 21, 2, 21 + 1), 'h'), + new Slash(new Range(2, 22, 2, 23)), + new Word(new Range(2, 23, 2, 23 + 2), 'to'), + new Slash(new Range(2, 25, 2, 26)), + new Word(new Range(2, 26, 2, 26 + 7), 'file.md'), new RightParenthesis(new Range(2, 33, 2, 34)), new NewLine(new Range(2, 34, 2, 35)), // `3nd` input line new LeftBracket(new Range(3, 1, 3, 2)), new Word(new Range(3, 2, 3, 2 + 4), 'text'), new RightBracket(new Range(3, 6, 3, 7)), - new stopCharacter(new Range(3, 7, 3, 8)), // <- stop character + new StopCharacter(new Range(3, 7, 3, 8)), // <- stop character new LeftParenthesis(new Range(3, 8, 3, 9)), - new Word(new Range(3, 9, 3, 9 + 5), '/etc/'), + new Slash(new Range(3, 9, 3, 10)), + new Word(new Range(3, 10, 3, 10 + 3), 'etc'), + new Slash(new Range(3, 13, 3, 14)), new Space(new Range(3, 14, 3, 15)), - new Word(new Range(3, 15, 3, 15 + 12), 'path/file.md'), + new Word(new Range(3, 15, 3, 15 + 4), 'path'), + new Slash(new Range(3, 19, 3, 20)), + new Word(new Range(3, 20, 3, 20 + 7), 'file.md'), new RightParenthesis(new Range(3, 27, 3, 28)), ], ); @@ -461,7 +506,7 @@ suite('MarkdownDecoder', () => { // space between caption and reference is disallowed '\f![link text] (./file path/name.jpg)', // new line inside the link reference - '\v![ ](./file\npath/name.jpg )', + '\v![ ](./file\npath/name.jpeg )', ]; await test.run( @@ -473,9 +518,15 @@ suite('MarkdownDecoder', () => { new Space(new Range(1, 3, 1, 4)), new RightBracket(new Range(1, 4, 1, 5)), new LeftParenthesis(new Range(1, 5, 1, 6)), - new Word(new Range(1, 6, 1, 6 + 11), './real/file'), + new Word(new Range(1, 6, 1, 6 + 1), '.'), + new Slash(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 8 + 4), 'real'), + new Slash(new Range(1, 12, 1, 13)), + new Word(new Range(1, 13, 1, 13 + 4), 'file'), new Space(new Range(1, 17, 1, 18)), - new Word(new Range(1, 18, 1, 18 + 19), 'path/file★name.webp'), + new Word(new Range(1, 18, 1, 18 + 4), 'path'), + new Slash(new Range(1, 22, 1, 23)), + new Word(new Range(1, 23, 1, 23 + 14), 'file★name.webp'), new NewLine(new Range(1, 37, 1, 38)), // `2nd` line new FormFeed(new Range(2, 1, 2, 2)), @@ -487,9 +538,13 @@ suite('MarkdownDecoder', () => { new RightBracket(new Range(2, 13, 2, 14)), new Space(new Range(2, 14, 2, 15)), new LeftParenthesis(new Range(2, 15, 2, 16)), - new Word(new Range(2, 16, 2, 16 + 6), './file'), + new Word(new Range(2, 16, 2, 16 + 1), '.'), + new Slash(new Range(2, 17, 2, 18)), + new Word(new Range(2, 18, 2, 18 + 4), 'file'), new Space(new Range(2, 22, 2, 23)), - new Word(new Range(2, 23, 2, 23 + 13), 'path/name.jpg'), + new Word(new Range(2, 23, 2, 23 + 4), 'path'), + new Slash(new Range(2, 27, 2, 28)), + new Word(new Range(2, 28, 2, 28 + 8), 'name.jpg'), new RightParenthesis(new Range(2, 36, 2, 37)), new NewLine(new Range(2, 37, 2, 38)), // `3rd` line @@ -499,23 +554,27 @@ suite('MarkdownDecoder', () => { new Space(new Range(3, 4, 3, 5)), new RightBracket(new Range(3, 5, 3, 6)), new LeftParenthesis(new Range(3, 6, 3, 7)), - new Word(new Range(3, 7, 3, 7 + 6), './file'), + new Word(new Range(3, 7, 3, 7 + 1), '.'), + new Slash(new Range(3, 8, 3, 9)), + new Word(new Range(3, 9, 3, 9 + 4), 'file'), new NewLine(new Range(3, 13, 3, 14)), - new Word(new Range(4, 1, 4, 1 + 13), 'path/name.jpg'), - new Space(new Range(4, 14, 4, 15)), - new RightParenthesis(new Range(4, 15, 4, 16)), + new Word(new Range(4, 1, 4, 1 + 4), 'path'), + new Slash(new Range(4, 5, 4, 6)), + new Word(new Range(4, 6, 4, 6 + 9), 'name.jpeg'), + new Space(new Range(4, 15, 4, 16)), + new RightParenthesis(new Range(4, 16, 4, 17)), ], ); }); suite('• stop characters inside caption/reference (new lines)', () => { - for (const stopCharacter of [CarriageReturn, NewLine]) { + for (const StopCharacter of [CarriageReturn, NewLine]) { let characterName = ''; - if (stopCharacter === CarriageReturn) { + if (StopCharacter === CarriageReturn) { characterName = '\\r'; } - if (stopCharacter === NewLine) { + if (StopCharacter === NewLine) { characterName = '\\n'; } @@ -531,11 +590,11 @@ suite('MarkdownDecoder', () => { const inputLines = [ // stop character inside link caption - `![haa${stopCharacter.symbol}loů](./real/💁/name.png)`, + `![haa${StopCharacter.symbol}loů](./real/💁/name.png)`, // stop character inside link reference - `![ref text](/etc/pat${stopCharacter.symbol}h/to/file.webp)`, + `![ref text](/etc/pat${StopCharacter.symbol}h/to/file.webp)`, // stop character between line caption and link reference is disallowed - `![text]${stopCharacter.symbol}(/etc/ path/file.jpeg)`, + `![text]${StopCharacter.symbol}(/etc/ path/file.jpeg)`, ]; @@ -546,11 +605,17 @@ suite('MarkdownDecoder', () => { new ExclamationMark(new Range(1, 1, 1, 2)), new LeftBracket(new Range(1, 2, 1, 3)), new Word(new Range(1, 3, 1, 3 + 3), 'haa'), - new stopCharacter(new Range(1, 6, 1, 7)), // <- stop character + new NewLine(new Range(1, 6, 1, 7)), // a single CR token is treated as a `new line` new Word(new Range(2, 1, 2, 1 + 3), 'loů'), new RightBracket(new Range(2, 4, 2, 5)), new LeftParenthesis(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.png'), + new Word(new Range(2, 6, 2, 6 + 1), '.'), + new Slash(new Range(2, 7, 2, 8)), + new Word(new Range(2, 8, 2, 8 + 4), 'real'), + new Slash(new Range(2, 12, 2, 13)), + new Word(new Range(2, 13, 2, 13 + 2), '💁'), + new Slash(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 8), 'name.png'), new RightParenthesis(new Range(2, 24, 2, 25)), new NewLine(new Range(2, 25, 2, 26)), // `2nd` input line @@ -561,9 +626,16 @@ suite('MarkdownDecoder', () => { new Word(new Range(3, 7, 3, 7 + 4), 'text'), new RightBracket(new Range(3, 11, 3, 12)), new LeftParenthesis(new Range(3, 12, 3, 13)), - new Word(new Range(3, 13, 3, 13 + 8), '/etc/pat'), - new stopCharacter(new Range(3, 21, 3, 22)), // <- stop character - new Word(new Range(4, 1, 4, 1 + 14), 'h/to/file.webp'), + new Slash(new Range(3, 13, 3, 14)), + new Word(new Range(3, 14, 3, 14 + 3), 'etc'), + new Slash(new Range(3, 17, 3, 18)), + new Word(new Range(3, 18, 3, 18 + 3), 'pat'), + new NewLine(new Range(3, 21, 3, 22)), // a single CR token is treated as a `new line` + new Word(new Range(4, 1, 4, 1 + 1), 'h'), + new Slash(new Range(4, 2, 4, 3)), + new Word(new Range(4, 3, 4, 3 + 2), 'to'), + new Slash(new Range(4, 5, 4, 6)), + new Word(new Range(4, 6, 4, 6 + 9), 'file.webp'), new RightParenthesis(new Range(4, 15, 4, 16)), new NewLine(new Range(4, 16, 4, 17)), // `3nd` input line @@ -571,11 +643,15 @@ suite('MarkdownDecoder', () => { new LeftBracket(new Range(5, 2, 5, 3)), new Word(new Range(5, 3, 5, 3 + 4), 'text'), new RightBracket(new Range(5, 7, 5, 8)), - new stopCharacter(new Range(5, 8, 5, 9)), // <- stop character + new NewLine(new Range(5, 8, 5, 9)), // a single CR token is treated as a `new line` new LeftParenthesis(new Range(6, 1, 6, 2)), - new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Slash(new Range(6, 2, 6, 3)), + new Word(new Range(6, 3, 6, 3 + 3), 'etc'), + new Slash(new Range(6, 6, 6, 7)), new Space(new Range(6, 7, 6, 8)), - new Word(new Range(6, 8, 6, 8 + 14), 'path/file.jpeg'), + new Word(new Range(6, 8, 6, 8 + 4), 'path'), + new Slash(new Range(6, 12, 6, 13)), + new Word(new Range(6, 13, 6, 13 + 9), 'file.jpeg'), new RightParenthesis(new Range(6, 22, 6, 23)), ], ); @@ -628,7 +704,13 @@ suite('MarkdownDecoder', () => { new Word(new Range(1, 7, 1, 7 + 3), 'loů'), new RightBracket(new Range(1, 10, 1, 11)), new LeftParenthesis(new Range(1, 11, 1, 12)), - new Word(new Range(1, 12, 1, 12 + 14), './real/💁/name'), + new Word(new Range(1, 12, 1, 12 + 1), '.'), + new Slash(new Range(1, 13, 1, 14)), + new Word(new Range(1, 14, 1, 14 + 4), 'real'), + new Slash(new Range(1, 18, 1, 19)), + new Word(new Range(1, 19, 1, 19 + 2), '💁'), + new Slash(new Range(1, 21, 1, 22)), + new Word(new Range(1, 22, 1, 22 + 4), 'name'), new RightParenthesis(new Range(1, 26, 1, 27)), new NewLine(new Range(1, 27, 1, 28)), // `2nd` input line @@ -639,9 +721,16 @@ suite('MarkdownDecoder', () => { new Word(new Range(2, 7, 2, 7 + 4), 'text'), new RightBracket(new Range(2, 11, 2, 12)), new LeftParenthesis(new Range(2, 12, 2, 13)), - new Word(new Range(2, 13, 2, 13 + 8), '/etc/pat'), + new Slash(new Range(2, 13, 2, 14)), + new Word(new Range(2, 14, 2, 14 + 3), 'etc'), + new Slash(new Range(2, 17, 2, 18)), + new Word(new Range(2, 18, 2, 18 + 3), 'pat'), new stopCharacter(new Range(2, 21, 2, 22)), // <- stop character - new Word(new Range(2, 22, 2, 22 + 14), 'h/to/file.webp'), + new Word(new Range(2, 22, 2, 22 + 1), 'h'), + new Slash(new Range(2, 23, 2, 24)), + new Word(new Range(2, 24, 2, 24 + 2), 'to'), + new Slash(new Range(2, 26, 2, 27)), + new Word(new Range(2, 27, 2, 27 + 9), 'file.webp'), new RightParenthesis(new Range(2, 36, 2, 37)), new NewLine(new Range(2, 37, 2, 38)), // `3nd` input line @@ -651,9 +740,13 @@ suite('MarkdownDecoder', () => { new RightBracket(new Range(3, 7, 3, 8)), new stopCharacter(new Range(3, 8, 3, 9)), // <- stop character new LeftParenthesis(new Range(3, 9, 3, 10)), - new Word(new Range(3, 10, 3, 10 + 5), '/etc/'), + new Slash(new Range(3, 10, 3, 11)), + new Word(new Range(3, 11, 3, 11 + 3), 'etc'), + new Slash(new Range(3, 14, 3, 15)), new Space(new Range(3, 15, 3, 16)), - new Word(new Range(3, 16, 3, 16 + 14), 'path/image.gif'), + new Word(new Range(3, 16, 3, 16 + 4), 'path'), + new Slash(new Range(3, 20, 3, 21)), + new Word(new Range(3, 21, 3, 21 + 9), 'image.gif'), new RightParenthesis(new Range(3, 30, 3, 31)), ], ); diff --git a/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts b/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts index 16ae699708c..7ebb92e68a0 100644 --- a/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts +++ b/code/src/vs/editor/test/common/codecs/simpleDecoder.test.ts @@ -3,25 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestDecoder } from '../utils/testDecoder.js'; import { Range } from '../../../common/core/range.js'; +import { TestDecoder } from '../utils/testDecoder.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { newWriteableStream } from '../../../../base/common/stream.js'; -import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; -import { Hash } from '../../../common/codecs/simpleCodec/tokens/hash.js'; -import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; -import { Dash } from '../../../common/codecs/simpleCodec/tokens/dash.js'; -import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalTab.js'; import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { SimpleDecoder, TSimpleToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; -import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; -import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; -import { LeftAngleBracket, RightAngleBracket } from '../../../common/codecs/simpleCodec/tokens/angleBrackets.js'; -import { ExclamationMark } from '../../../common/codecs/simpleCodec/tokens/exclamationMark.js'; +import { SimpleDecoder, TSimpleDecoderToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { + At, + Tab, + Word, + Hash, + Dash, + Colon, + Slash, + Space, + Quote, + FormFeed, + DollarSign, + DoubleQuote, + VerticalTab, + LeftBracket, + RightBracket, + LeftCurlyBrace, + RightCurlyBrace, + ExclamationMark, + LeftParenthesis, + RightParenthesis, + LeftAngleBracket, + RightAngleBracket, + Comma, +} from '../../../common/codecs/simpleCodec/tokens/index.js'; /** * A reusable test utility that asserts that a `SimpleDecoder` instance @@ -45,7 +59,7 @@ import { ExclamationMark } from '../../../common/codecs/simpleCodec/tokens/excla * ], * ); */ -export class TestSimpleDecoder extends TestDecoder { +export class TestSimpleDecoder extends TestDecoder { constructor() { const stream = newWriteableStream(null); const decoder = new SimpleDecoder(stream); @@ -57,13 +71,22 @@ export class TestSimpleDecoder extends TestDecoder suite('SimpleDecoder', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - test('produces expected tokens', async () => { + test('produces expected tokens #1', async () => { const test = testDisposables.add( new TestSimpleDecoder(), ); await test.run( - ' hello world!\nhow are\t you?\v\n\n (test) [!@#$%^🦄&*_+=-]\f \n\t\t🤗❤ \t\n hey\v-\tthere\r\n\r\n', + [ + ' hello world!', + 'how are\t "you?"\v', + '', + ' (test) [!@#$:%^🦄&*_+=,-,]\f ', + '\t\t🤗❤ \t', + ' hey\v-\tthere\r', + ' @workspace@legomushroom', + '\'my\' ${text} /run', + ], [ // first line new Space(new Range(1, 1, 1, 2)), @@ -78,9 +101,11 @@ suite('SimpleDecoder', () => { new Word(new Range(2, 5, 2, 8), 'are'), new Tab(new Range(2, 8, 2, 9)), new Space(new Range(2, 9, 2, 10)), - new Word(new Range(2, 10, 2, 14), 'you?'), - new VerticalTab(new Range(2, 14, 2, 15)), - new NewLine(new Range(2, 15, 2, 16)), + new DoubleQuote(new Range(2, 10, 2, 11)), + new Word(new Range(2, 11, 2, 11 + 4), 'you?'), + new DoubleQuote(new Range(2, 15, 2, 16)), + new VerticalTab(new Range(2, 16, 2, 17)), + new NewLine(new Range(2, 17, 2, 18)), // third line new NewLine(new Range(3, 1, 3, 2)), // fourth line @@ -94,15 +119,19 @@ suite('SimpleDecoder', () => { new Space(new Range(4, 11, 4, 12)), new LeftBracket(new Range(4, 12, 4, 13)), new ExclamationMark(new Range(4, 13, 4, 14)), - new Word(new Range(4, 14, 4, 15), '@'), + new At(new Range(4, 14, 4, 15)), new Hash(new Range(4, 15, 4, 16)), - new Word(new Range(4, 16, 4, 16 + 10), '$%^🦄&*_+='), - new Dash(new Range(4, 26, 4, 27)), - new RightBracket(new Range(4, 27, 4, 28)), - new FormFeed(new Range(4, 28, 4, 29)), - new Space(new Range(4, 29, 4, 30)), - new Space(new Range(4, 30, 4, 31)), - new NewLine(new Range(4, 31, 4, 32)), + new DollarSign(new Range(4, 16, 4, 17)), + new Colon(new Range(4, 17, 4, 18)), + new Word(new Range(4, 18, 4, 18 + 9), '%^🦄&*_+='), + new Comma(new Range(4, 27, 4, 28)), + new Dash(new Range(4, 28, 4, 29)), + new Comma(new Range(4, 29, 4, 30)), + new RightBracket(new Range(4, 30, 4, 31)), + new FormFeed(new Range(4, 31, 4, 32)), + new Space(new Range(4, 32, 4, 33)), + new Space(new Range(4, 33, 4, 34)), + new NewLine(new Range(4, 34, 4, 35)), // fifth line new Tab(new Range(5, 1, 5, 2)), new LeftAngleBracket(new Range(5, 2, 5, 3)), @@ -125,8 +154,84 @@ suite('SimpleDecoder', () => { new CarriageReturn(new Range(6, 13, 6, 14)), new NewLine(new Range(6, 14, 6, 15)), // seventh line - new CarriageReturn(new Range(7, 1, 7, 2)), - new NewLine(new Range(7, 2, 7, 3)), + new Space(new Range(7, 1, 7, 2)), + new At(new Range(7, 2, 7, 3)), + new Word(new Range(7, 3, 7, 12), 'workspace'), + new At(new Range(7, 12, 7, 13)), + new Word(new Range(7, 13, 7, 25), 'legomushroom'), + new NewLine(new Range(7, 25, 7, 26)), + // eighth line + new Quote(new Range(8, 1, 8, 2)), + new Word(new Range(8, 2, 8, 2 + 2), 'my'), + new Quote(new Range(8, 4, 8, 5)), + new Space(new Range(8, 5, 8, 6)), + new DollarSign(new Range(8, 6, 8, 7)), + new LeftCurlyBrace(new Range(8, 7, 8, 8)), + new Word(new Range(8, 8, 8, 8 + 4), 'text'), + new RightCurlyBrace(new Range(8, 12, 8, 13)), + new Space(new Range(8, 13, 8, 14)), + new Slash(new Range(8, 14, 8, 15)), + new Word(new Range(8, 15, 8, 15 + 3), 'run'), + ], + ); + }); + + test('produces expected tokens #2', async () => { + const test = testDisposables.add( + new TestSimpleDecoder(), + ); + + await test.run( + [ + 'your command is /catch', + '\t\t/command1/command2 ', + ' /cmd#var ', + '/test@github\t\t', + '/update\r', + '', + ], + [ + // first line + new Word(new Range(1, 1, 1, 5), 'your'), + new Space(new Range(1, 5, 1, 6)), + new Word(new Range(1, 6, 1, 6 + 7), 'command'), + new Space(new Range(1, 13, 1, 14)), + new Word(new Range(1, 14, 1, 14 + 2), 'is'), + new Space(new Range(1, 16, 1, 17)), + new Slash(new Range(1, 17, 1, 18)), + new Word(new Range(1, 18, 1, 18 + 5), 'catch'), + new NewLine(new Range(1, 23, 1, 24)), + // second line + new Tab(new Range(2, 1, 2, 2)), + new Tab(new Range(2, 2, 2, 3)), + new Slash(new Range(2, 3, 2, 4)), + new Word(new Range(2, 4, 2, 4 + 8), 'command1'), + new Slash(new Range(2, 12, 2, 13)), + new Word(new Range(2, 13, 2, 13 + 8), 'command2'), + new Space(new Range(2, 21, 2, 22)), + new NewLine(new Range(2, 22, 2, 23)), + // third line + new Space(new Range(3, 1, 3, 2)), + new Space(new Range(3, 2, 3, 3)), + new Slash(new Range(3, 3, 3, 4)), + new Word(new Range(3, 4, 3, 4 + 3), 'cmd'), + new Hash(new Range(3, 7, 3, 8)), + new Word(new Range(3, 8, 3, 8 + 3), 'var'), + new Space(new Range(3, 11, 3, 12)), + new NewLine(new Range(3, 12, 3, 13)), + // fourth line + new Slash(new Range(4, 1, 4, 2)), + new Word(new Range(4, 2, 4, 2 + 4), 'test'), + new At(new Range(4, 6, 4, 7)), + new Word(new Range(4, 7, 4, 7 + 6), 'github'), + new Tab(new Range(4, 13, 4, 14)), + new Tab(new Range(4, 14, 4, 15)), + new NewLine(new Range(4, 15, 4, 16)), + // fifth line + new Slash(new Range(5, 1, 5, 2)), + new Word(new Range(5, 2, 5, 2 + 6), 'update'), + new CarriageReturn(new Range(5, 8, 5, 9)), + new NewLine(new Range(5, 9, 5, 10)), ], ); }); diff --git a/code/src/vs/editor/test/common/utils/testDecoder.ts b/code/src/vs/editor/test/common/utils/testDecoder.ts index 78ce39a310b..28b5130fa26 100644 --- a/code/src/vs/editor/test/common/utils/testDecoder.ts +++ b/code/src/vs/editor/test/common/utils/testDecoder.ts @@ -102,45 +102,8 @@ export class TestDecoder> extends // initiate the data sending flow this.sendData(inputData); - // consume the decoder tokens based on specified - // (or randomly generated) tokens consume method - const receivedTokens: T[] = []; - switch (tokensConsumeMethod) { - // test the `async iterator` code path - case 'async-generator': { - for await (const token of this.decoder) { - if (token === null) { - break; - } - - receivedTokens.push(token); - } - - break; - } - // test the `.consumeAll()` method code path - case 'consume-all-method': { - receivedTokens.push(...(await this.decoder.consumeAll())); - break; - } - // test the `.onData()` event consume flow - case 'on-data-event': { - this.decoder.onData((token) => { - receivedTokens.push(token); - }); - - this.decoder.start(); - - // in this case we also test the `settled` promise of the decoder - await this.decoder.settled; - - break; - } - // ensure that the switch block is exhaustive - default: { - throw new Error(`Unknown consume method '${tokensConsumeMethod}'.`); - } - } + // receive tokens from the decoder stream + const receivedTokens = await this.receiveTokens(tokensConsumeMethod); // validate the received tokens this.validateReceivedTokens( @@ -191,6 +154,55 @@ export class TestDecoder> extends } } + /** + * Receive all tokens from the decoder stream using the specified consume method. + */ + public async receiveTokens( + tokensConsumeMethod: TTokensConsumeMethod = this.randomTokensConsumeMethod(), + ): Promise { + // consume the decoder tokens based on specified + // (or randomly generated) tokens consume method + const receivedTokens: T[] = []; + switch (tokensConsumeMethod) { + // test the `async iterator` code path + case 'async-generator': { + for await (const token of this.decoder) { + if (token === null) { + break; + } + + receivedTokens.push(token); + } + + break; + } + // test the `.consumeAll()` method code path + case 'consume-all-method': { + receivedTokens.push(...(await this.decoder.consumeAll())); + break; + } + // test the `.onData()` event consume flow + case 'on-data-event': { + this.decoder.onData((token) => { + receivedTokens.push(token); + }); + + this.decoder.start(); + + // in this case we also test the `settled` promise of the decoder + await this.decoder.settled; + + break; + } + // ensure that the switch block is exhaustive + default: { + throw new Error(`Unknown consume method '${tokensConsumeMethod}'.`); + } + } + + return receivedTokens; + } + /** * Validate that received tokens list is equal to the expected one. */ @@ -209,7 +221,7 @@ export class TestDecoder> extends assert( receivedToken.equals(expectedToken), - `Expected token '${i}' to be '${expectedToken}', got '${receivedToken}'.`, + `\nExpected token '${i}' to be:\n\n${expectedToken.text}\n(${expectedToken.range})\n\ngot:\n\n${receivedToken.text}\n(${receivedToken.range})\n`, ); } diff --git a/code/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/code/src/vs/editor/test/common/viewLayout/lineHeights.test.ts new file mode 100644 index 00000000000..0047a4014ba --- /dev/null +++ b/code/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { LineHeightsManager } from '../../../common/viewLayout/lineHeights.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; + +suite('Editor ViewLayout - LineHeightsManager', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('default line height is used when no custom heights exist', () => { + const manager = new LineHeightsManager(10, []); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(5), 10); + assert.strictEqual(manager.heightForLineNumber(100), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 50); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(10), 100); + }); + + test('can change default line height', () => { + const manager = new LineHeightsManager(10, []); + manager.defaultLineHeight = 20; + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 20); + assert.strictEqual(manager.heightForLineNumber(5), 20); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 100); + }); + + test('can add single custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 10); + assert.strictEqual(manager.heightForLineNumber(3), 20); + assert.strictEqual(manager.heightForLineNumber(4), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 40); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 50); + }); + + test('can add multiple custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 15); + manager.insertOrChangeCustomLineHeight('dec2', 4, 4, 25); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 15); + assert.strictEqual(manager.heightForLineNumber(3), 10); + assert.strictEqual(manager.heightForLineNumber(4), 25); + assert.strictEqual(manager.heightForLineNumber(5), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 25); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 35); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 60); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 70); + }); + + test('can add range of custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 2, 4, 15); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 15); + assert.strictEqual(manager.heightForLineNumber(3), 15); + assert.strictEqual(manager.heightForLineNumber(4), 15); + assert.strictEqual(manager.heightForLineNumber(5), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 25); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 40); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 55); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 65); + }); + + test('can change existing custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 20); + + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 30); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 30); + + // Check accumulated heights after change + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 50); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 60); + }); + + test('can remove custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 20); + + manager.removeCustomLineHeight('dec1'); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 10); + + // Check accumulated heights after removal + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 30); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 40); + }); + + test('handles overlapping custom line heights (last one wins)', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 5, 20); + manager.insertOrChangeCustomLineHeight('dec2', 4, 6, 30); + manager.commit(); + + assert.strictEqual(manager.heightForLineNumber(2), 10); + assert.strictEqual(manager.heightForLineNumber(3), 20); + assert.strictEqual(manager.heightForLineNumber(4), 30); + assert.strictEqual(manager.heightForLineNumber(5), 30); + assert.strictEqual(manager.heightForLineNumber(6), 30); + assert.strictEqual(manager.heightForLineNumber(7), 10); + }); + + test('handles deleting lines before custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 10, 12, 20); + manager.commit(); + + manager.onLinesDeleted(5, 7); // Delete lines 5-7 + + assert.strictEqual(manager.heightForLineNumber(7), 20); // Was line 10 + assert.strictEqual(manager.heightForLineNumber(8), 20); // Was line 11 + assert.strictEqual(manager.heightForLineNumber(9), 20); // Was line 12 + assert.strictEqual(manager.heightForLineNumber(10), 10); + }); + + test('handles deleting lines overlapping with custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 10, 20); + manager.commit(); + + manager.onLinesDeleted(7, 12); // Delete lines 7-12, including part of decoration + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 10); + }); + + test('handles deleting lines containing custom line heights completely', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesDeleted(4, 8); // Delete lines 4-8, completely contains decoration + + // The decoration collapses to a single line which matches the behavior in the text buffer + assert.strictEqual(manager.heightForLineNumber(3), 10); + assert.strictEqual(manager.heightForLineNumber(4), 20); + assert.strictEqual(manager.heightForLineNumber(5), 10); + }); + + test('handles inserting lines before custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesInserted(3, 4); // Insert 2 lines at line 3 + + assert.strictEqual(manager.heightForLineNumber(5), 10); + assert.strictEqual(manager.heightForLineNumber(6), 10); + assert.strictEqual(manager.heightForLineNumber(7), 20); // Was line 5 + assert.strictEqual(manager.heightForLineNumber(8), 20); // Was line 6 + assert.strictEqual(manager.heightForLineNumber(9), 20); // Was line 7 + }); + + test('handles inserting lines inside custom line heights range', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesInserted(6, 7); // Insert 2 lines at line 6 + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 20); + assert.strictEqual(manager.heightForLineNumber(8), 20); + assert.strictEqual(manager.heightForLineNumber(9), 20); + }); + + test('changing decoration id maintains custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.removeCustomLineHeight('dec1'); + manager.insertOrChangeCustomLineHeight('dec2', 5, 7, 20); + manager.commit(); + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 20); + }); + + test('accumulates heights correctly with complex setup', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 15); + manager.insertOrChangeCustomLineHeight('dec2', 5, 7, 20); + manager.insertOrChangeCustomLineHeight('dec3', 10, 10, 30); + manager.commit(); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 35); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 45); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 65); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(7), 105); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(9), 125); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(10), 155); + }); + + test('partial deletion with multiple lines for the same decoration ID', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('decSame', 5, 5, 20); + manager.insertOrChangeCustomLineHeight('decSame', 6, 6, 25); + manager.commit(); + + // Delete one line that partially intersects the same decoration + manager.onLinesDeleted(6, 6); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 10); + }); + + test('overlapping decorations use maximum line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('decA', 3, 5, 40); + manager.insertOrChangeCustomLineHeight('decB', 4, 6, 30); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(3), 40); + assert.strictEqual(manager.heightForLineNumber(4), 40); + assert.strictEqual(manager.heightForLineNumber(5), 40); + assert.strictEqual(manager.heightForLineNumber(6), 30); + }); +}); diff --git a/code/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/code/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 087c24e457b..7bf20a78d84 100644 --- a/code/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/code/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -33,7 +33,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout 1', () => { // Start off with 10 lines - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - @@ -142,7 +142,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout 2', () => { // Start off with 10 lines and one whitespace after line 2, of height 5 - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); const a = insertWhitespace(linesLayout, 2, 0, 5, 0); // 10 lines @@ -239,7 +239,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout Padding', () => { // Start off with 10 lines - const linesLayout = new LinesLayout(10, 10, 15, 20); + const linesLayout = new LinesLayout(10, 10, 15, 20, []); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - @@ -333,7 +333,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines @@ -382,7 +382,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getCenteredLineInViewport', () => { - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines @@ -465,7 +465,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLinesViewportData 1', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 100, 0); // 10 lines @@ -598,7 +598,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); const a = insertWhitespace(linesLayout, 6, 0, 100, 0); const b = insertWhitespace(linesLayout, 7, 0, 50, 0); @@ -669,7 +669,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getWhitespaceAtVerticalOffset', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); const a = insertWhitespace(linesLayout, 6, 0, 100, 0); const b = insertWhitespace(linesLayout, 7, 0, 50, 0); @@ -712,7 +712,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); // Insert a whitespace after line number 2, of height 10 const a = insertWhitespace(linesLayout, 2, 0, 10, 0); @@ -1063,7 +1063,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); @@ -1187,7 +1187,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout Bug', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); diff --git a/code/src/vs/monaco.d.ts b/code/src/vs/monaco.d.ts index 49e44eaab8a..a5ce6e74f66 100644 --- a/code/src/vs/monaco.d.ts +++ b/code/src/vs/monaco.d.ts @@ -518,6 +518,7 @@ declare namespace monaco { readonly altKey: boolean; readonly metaKey: boolean; readonly timestamp: number; + readonly defaultPrevented: boolean; preventDefault(): void; stopPropagation(): void; } @@ -1743,6 +1744,10 @@ declare namespace monaco.editor { * with the specified {@link IModelDecorationGlyphMarginOptions} in the glyph margin. */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; + /** + * If set, the decoration will override the line height of the lines it spans. + */ + lineHeight?: number | null; /** * If set, the decoration will be rendered in the lines decorations with this CSS class name. */ @@ -2277,6 +2282,11 @@ declare namespace monaco.editor { * @param ownerId If set, it will ignore decorations belonging to other owners. */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ @@ -6099,6 +6109,10 @@ declare namespace monaco.editor { * Get the vertical position (top offset) for the position w.r.t. to the first line. */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the line height for the line number. + */ + getLineHeightForLineNumber(lineNumber: number): number; /** * Write the screen reader content to be the current selection */ @@ -7307,6 +7321,7 @@ declare namespace monaco.languages { readonly showInlineEditMenu?: boolean; readonly showRange?: IRange; readonly warning?: InlineCompletionWarning; + readonly displayLocation?: InlineCompletionDisplayLocation; } export interface InlineCompletionWarning { @@ -7314,6 +7329,11 @@ declare namespace monaco.languages { icon?: IconPath; } + export interface InlineCompletionDisplayLocation { + range: IRange; + label: string; + } + /** * TODO: add `| Uri | { light: Uri; dark: Uri }`. */ @@ -7346,11 +7366,20 @@ declare namespace monaco.languages { * @param acceptedCharacters Deprecated. Use `info.acceptedCharacters` instead. */ handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; + /** + * @deprecated Use `handleEndOfLifetime` instead. + */ handleRejection?(completions: T, item: T['items'][number]): void; + /** + * Is called when an inline completion item is no longer being used. + * Provides a reason of why it is not used anymore. + */ + handleEndOfLifetime?(completions: T, item: T['items'][number], reason: InlineCompletionEndOfLifeReason): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ freeInlineCompletions(completions: T): void; + onDidChangeInlineCompletions?: IEvent; /** * Only used for {@link yieldsToGroupIds}. * Multiple providers can have the same group id. @@ -7366,6 +7395,22 @@ declare namespace monaco.languages { toString?(): string; } + export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2 + } + + export type InlineCompletionEndOfLifeReason = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; + } | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; + } | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: TInlineCompletion; + userTypingDisagreed: boolean; + }; + export interface CodeAction { title: string; command?: Command; diff --git a/code/src/vs/platform/accessibility/test/common/testAccessibilityService.ts b/code/src/vs/platform/accessibility/test/common/testAccessibilityService.ts index 3357330e6cd..4f21111492e 100644 --- a/code/src/vs/platform/accessibility/test/common/testAccessibilityService.ts +++ b/code/src/vs/platform/accessibility/test/common/testAccessibilityService.ts @@ -14,7 +14,7 @@ export class TestAccessibilityService implements IAccessibilityService { onDidChangeReducedMotion = Event.None; isScreenReaderOptimized(): boolean { return false; } - isMotionReduced(): boolean { return false; } + isMotionReduced(): boolean { return true; } alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } diff --git a/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 009c85db29a..128c3fe2064 100644 --- a/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/code/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -318,6 +318,8 @@ export class Sound { public static readonly chatEditModifiedFile = Sound.register({ fileName: 'chatEditModifiedFile.mp3' }); public static readonly editsKept = Sound.register({ fileName: 'editsKept.mp3' }); public static readonly editsUndone = Sound.register({ fileName: 'editsUndone.mp3' }); + public static readonly nextEditSuggestion = Sound.register({ fileName: 'nextEditSuggestion.mp3' }); + public static readonly terminalCommandSucceeded = Sound.register({ fileName: 'terminalCommandSucceeded.mp3' }); private constructor(public readonly fileName: string) { } } @@ -434,7 +436,13 @@ export class AccessibilitySignal { legacySoundSettingsKey: 'audioCues.lineHasInlineSuggestion', settingsKey: 'accessibility.signals.lineHasInlineSuggestion', }); - + public static readonly nextEditSuggestion = AccessibilitySignal.register({ + name: localize('accessibilitySignals.nextEditSuggestion.name', 'Next Edit Suggestion on Line'), + sound: Sound.nextEditSuggestion, + legacySoundSettingsKey: 'audioCues.nextEditSuggestion', + settingsKey: 'accessibility.signals.nextEditSuggestion', + announcementMessage: localize('accessibility.signals.nextEditSuggestion', 'Next Edit Suggestion'), + }); public static readonly terminalQuickFix = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalQuickFix.name', 'Terminal Quick Fix'), sound: Sound.quickFixes, @@ -491,7 +499,7 @@ export class AccessibilitySignal { public static readonly terminalCommandSucceeded = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'), - sound: Sound.success, + sound: Sound.terminalCommandSucceeded, announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'), settingsKey: 'accessibility.signals.terminalCommandSucceeded', }); diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 new file mode 100644 index 00000000000..ae39e3aaf41 Binary files /dev/null and b/code/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 differ diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 index 68a9cc83565..147f81af550 100644 Binary files a/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 and b/code/src/vs/platform/accessibilitySignal/browser/media/save.mp3 differ diff --git a/code/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 b/code/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 new file mode 100644 index 00000000000..68a9cc83565 Binary files /dev/null and b/code/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 differ diff --git a/code/src/vs/platform/actionWidget/browser/actionList.ts b/code/src/vs/platform/actionWidget/browser/actionList.ts index ff973d299fe..4205ba69d1e 100644 --- a/code/src/vs/platform/actionWidget/browser/actionList.ts +++ b/code/src/vs/platform/actionWidget/browser/actionList.ts @@ -36,15 +36,18 @@ export interface IActionListItem { readonly group?: { kind?: any; icon?: ThemeIcon; title: string }; readonly disabled?: boolean; readonly label?: string; + readonly description?: string; readonly keybinding?: ResolvedKeybinding; canPreview?: boolean | undefined; readonly hideIcon?: boolean; + readonly tooltip?: string; } interface IActionMenuTemplateData { readonly container: HTMLElement; readonly icon: HTMLElement; readonly text: HTMLElement; + readonly description?: HTMLElement; readonly keybinding: KeybindingLabel; } @@ -72,7 +75,7 @@ class HeaderRenderer implements IListRenderer, IHeaderTemp } renderElement(element: IActionListItem, _index: number, templateData: IHeaderTemplateData): void { - templateData.text.textContent = element.group?.title ?? ''; + templateData.text.textContent = element.group?.title ?? element.label ?? ''; } disposeTemplate(_templateData: IHeaderTemplateData): void { @@ -100,9 +103,13 @@ class ActionItemRenderer implements IListRenderer, IAction text.className = 'title'; container.append(text); + const description = document.createElement('span'); + description.className = 'description'; + container.append(description); + const keybinding = new KeybindingLabel(container, OS); - return { container, icon, text, keybinding }; + return { container, icon, text, description, keybinding }; } renderElement(element: IActionListItem, _index: number, data: IActionMenuTemplateData): void { @@ -124,13 +131,23 @@ class ActionItemRenderer implements IListRenderer, IAction data.text.textContent = stripNewlines(element.label); + if (element.description) { + data.description!.textContent = stripNewlines(element.description); + data.description!.style.display = 'inline'; + } else { + data.description!.textContent = ''; + data.description!.style.display = 'none'; + } + data.keybinding.set(element.keybinding); dom.setVisibility(!!element.keybinding, data.keybinding.element); const actionTitle = this._keybindingService.lookupKeybinding(acceptSelectedActionCommand)?.getLabel(); const previewTitle = this._keybindingService.lookupKeybinding(previewSelectedActionCommand)?.getLabel(); data.container.classList.toggle('option-disabled', element.disabled); - if (element.disabled) { + if (element.tooltip) { + data.container.title = element.tooltip; + } else if (element.disabled) { data.container.title = element.label; } else if (actionTitle && previewTitle) { if (this._supportsPreview && element.canPreview) { diff --git a/code/src/vs/platform/actionWidget/browser/actionWidget.css b/code/src/vs/platform/actionWidget/browser/actionWidget.css index d205c7ab791..4cbb3277f57 100644 --- a/code/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/code/src/vs/platform/actionWidget/browser/actionWidget.css @@ -6,7 +6,7 @@ .action-widget { font-size: 13px; border-radius: 0; - min-width: 160px; + min-width: 100px; max-width: 80vw; z-index: 40; display: block; @@ -168,3 +168,9 @@ /* The important gives this rule precedence over the hover rule. */ background: var(--vscode-actionBar-toggledBackground) !important; } + +.action-widget .monaco-list .monaco-list-row .description { + opacity: 0.7; + margin-left: 0.5em; + font-size: 0.9em; +} diff --git a/code/src/vs/platform/actionWidget/browser/actionWidget.ts b/code/src/vs/platform/actionWidget/browser/actionWidget.ts index 450126a0f27..43179ddc230 100644 --- a/code/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/code/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -18,6 +18,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common import { createDecorator, IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js'; import { inputActiveOptionBackground, registerColor } from '../../theme/common/colorRegistry.js'; +import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; registerColor( 'actionBar.toggledBackground', @@ -34,7 +35,7 @@ export const IActionWidgetService = createDecorator('actio export interface IActionWidgetService { readonly _serviceBrand: undefined; - show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void; + show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void; hide(didCancel?: boolean): void; @@ -58,7 +59,7 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { super(); } - show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void { + show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void { const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService); const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate); diff --git a/code/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/code/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts new file mode 100644 index 00000000000..535c11c5758 --- /dev/null +++ b/code/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionWidgetService } from './actionWidget.js'; +import { IAction } from '../../../base/common/actions.js'; +import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js'; +import { ThemeIcon } from '../../../base/common/themables.js'; +import { Codicon } from '../../../base/common/codicons.js'; +import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js'; +import { IKeybindingService } from '../../keybinding/common/keybinding.js'; + +export interface IActionWidgetDropdownAction extends IAction { + category?: { label: string; order: number }; + description?: string; +} + +// TODO @lramos15 - Should we just make IActionProvider templated? +export interface IActionWidgetDropdownActionProvider { + getActions(): IActionWidgetDropdownAction[]; +} + +export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions { + // These are the actions that are shown in the action widget split up by category + readonly actions?: IActionWidgetDropdownAction[]; + readonly actionProvider?: IActionWidgetDropdownActionProvider; + + // These actions are those shown at the bottom of the action widget + readonly actionBarActions?: IAction[]; + readonly actionBarActionProvider?: IActionProvider; + readonly showItemKeybindings?: boolean; +} + +/** + * Action widget dropdown is a dropdown that uses the action widget under the hood to simulate a native dropdown menu + * The benefits of this include non native features such as headers, descriptions, icons, and button bar + */ +export class ActionWidgetDropdown extends BaseDropdown { + constructor( + container: HTMLElement, + private readonly _options: IActionWidgetDropdownOptions, + @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + ) { + super(container, _options); + } + + override show(): void { + let actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? []; + const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? []; + const actionWidgetItems: IActionListItem[] = []; + + const actionsByCategory = new Map(); + for (const action of actions) { + let category = action.category; + if (!category) { + category = { label: '', order: Number.MAX_SAFE_INTEGER }; + } + if (!actionsByCategory.has(category.label)) { + actionsByCategory.set(category.label, []); + } + actionsByCategory.get(category.label)!.push(action); + } + + // Sort categories by order + const sortedCategories = Array.from(actionsByCategory.entries()) + .sort((a, b) => { + const aOrder = a[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER; + const bOrder = b[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER; + return aOrder - bOrder; + }); + + for (const [categoryLabel, categoryActions] of sortedCategories) { + + if (categoryLabel) { + // Push headers for each category + actionWidgetItems.push({ + label: categoryLabel, + kind: ActionListItemKind.Header, + canPreview: false, + disabled: false, + hideIcon: false, + }); + } + // Push actions for each category + for (const action of categoryActions) { + actionWidgetItems.push({ + item: action, + tooltip: action.tooltip, + description: action.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: action.label, + keybinding: this._options.showItemKeybindings ? + this.keybindingService.lookupKeybinding(action.id) : + undefined, + }); + } + } + + const previouslyFocusedElement = getActiveElement(); + + + const actionWidgetDelegate: IActionListDelegate = { + onSelect: (action, preview) => { + this.actionWidgetService.hide(); + action.run(); + }, + onHide: () => { + if (isHTMLElement(previouslyFocusedElement)) { + previouslyFocusedElement.focus(); + } + } + }; + + actionBarActions = actionBarActions.map(action => ({ + ...action, + run: async (...args: any[]) => { + this.actionWidgetService.hide(); + return action.run(...args); + } + })); + + this.actionWidgetService.show( + this._options.label ?? '', + false, + actionWidgetItems, + actionWidgetDelegate, + this.element, + undefined, + actionBarActions + ); + } +} diff --git a/code/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts b/code/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts new file mode 100644 index 00000000000..bd0d57848a8 --- /dev/null +++ b/code/src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, append } from '../../../base/browser/dom.js'; +import { BaseActionViewItem } from '../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ILabelRenderer } from '../../../base/browser/ui/dropdown/dropdown.js'; +import { getBaseLayerHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate2.js'; +import { getDefaultHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { IAction } from '../../../base/common/actions.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; +import { IActionWidgetService } from '../../actionWidget/browser/actionWidget.js'; +import { ActionWidgetDropdown, IActionWidgetDropdownOptions } from '../../actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../keybinding/common/keybinding.js'; + +/** + * Action view item for the custom action widget dropdown widget. + * Very closely based off of `DropdownMenuActionViewItem`, would be good to have some code re-use in the future + */ +export class ActionWidgetDropdownActionViewItem extends BaseActionViewItem { + private actionWidgetDropdown: ActionWidgetDropdown | undefined; + private actionItem: HTMLElement | null = null; + constructor( + action: IAction, + private readonly actionWidgetOptions: Omit, + @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { + super(undefined, action); + } + + override render(container: HTMLElement): void { + this.actionItem = container; + + const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { + this.element = append(el, $('a.action-label')); + return this.renderLabel(this.element); + }; + + this.actionWidgetDropdown = this._register(new ActionWidgetDropdown(container, { ...this.actionWidgetOptions, labelRenderer }, this._actionWidgetService, this._keybindingService)); + this._register(this.actionWidgetDropdown.onDidChangeVisibility(visible => { + this.element?.setAttribute('aria-expanded', `${visible}`); + })); + + this.updateTooltip(); + this.updateEnabled(); + } + + protected renderLabel(element: HTMLElement): IDisposable | null { + // todo@aeschli: remove codicon, should come through `this.options.classNames` + element.classList.add('codicon'); + + if (this._action.label) { + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label)); + } + + return null; + } + + protected setAriaLabelAttributes(element: HTMLElement): void { + element.setAttribute('role', 'button'); + element.setAttribute('aria-haspopup', 'true'); + element.setAttribute('aria-expanded', 'false'); + element.ariaLabel = this._action.label || ''; + } + + protected override getTooltip() { + const keybinding = this._keybindingService.lookupKeybinding(this.action.id, this._contextKeyService); + const keybindingLabel = keybinding && keybinding.getLabel(); + + const tooltip = this.action.tooltip ?? this.action.label; + return keybindingLabel + ? `${tooltip} (${keybindingLabel})` + : tooltip; + } + + show(): void { + this.actionWidgetDropdown?.show(); + } + + protected override updateEnabled(): void { + const disabled = !this.action.enabled; + this.actionItem?.classList.toggle('disabled', disabled); + this.element?.classList.toggle('disabled', disabled); + } + +} diff --git a/code/src/vs/platform/actions/browser/buttonbar.ts b/code/src/vs/platform/actions/browser/buttonbar.ts index e477d3ddbdd..1e29718f04a 100644 --- a/code/src/vs/platform/actions/browser/buttonbar.ts +++ b/code/src/vs/platform/actions/browser/buttonbar.ts @@ -85,7 +85,13 @@ export class WorkbenchButtonBar extends ButtonBar { const actionOrSubmenu = actions[i]; let action: IAction; let btn: IButton; - + let tooltip: string = ''; + const kb = actionOrSubmenu instanceof SubmenuAction ? '' : this._keybindingService.lookupKeybinding(actionOrSubmenu.id); + if (kb) { + tooltip = localize('labelWithKeybinding', "{0} ({1})", actionOrSubmenu.tooltip || actionOrSubmenu.label, kb.getLabel()); + } else { + tooltip = actionOrSubmenu.tooltip || actionOrSubmenu.label; + } if (actionOrSubmenu instanceof SubmenuAction && actionOrSubmenu.actions.length > 0) { const [first, ...rest] = actionOrSubmenu.actions; action = first; @@ -94,14 +100,14 @@ export class WorkbenchButtonBar extends ButtonBar { actionRunner: this._actionRunner, actions: rest, contextMenuProvider: this._contextMenuService, - ariaLabel: action.label, + ariaLabel: tooltip, supportIcons: true, }); } else { action = actionOrSubmenu; btn = this.addButton({ secondary: conifgProvider(action, i)?.isSecondary ?? secondary, - ariaLabel: action.label, + ariaLabel: tooltip, supportIcons: true, }); } @@ -128,13 +134,7 @@ export class WorkbenchButtonBar extends ButtonBar { btn.element.classList.add(...action.class.split(' ')); } } - const kb = this._keybindingService.lookupKeybinding(action.id); - let tooltip: string; - if (kb) { - tooltip = localize('labelWithKeybinding', "{0} ({1})", action.tooltip || action.label, kb.getLabel()); - } else { - tooltip = action.tooltip || action.label; - } + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, tooltip)); this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); diff --git a/code/src/vs/platform/actions/common/actions.ts b/code/src/vs/platform/actions/common/actions.ts index eee25b6d57a..c143e4ee1e2 100644 --- a/code/src/vs/platform/actions/common/actions.ts +++ b/code/src/vs/platform/actions/common/actions.ts @@ -60,6 +60,7 @@ export class MenuId { static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); static readonly DebugToolBarStop = new MenuId('DebugToolBarStop'); + static readonly DebugDisassemblyContext = new MenuId('DebugDisassemblyContext'); static readonly DebugCallStackToolbar = new MenuId('DebugCallStackToolbar'); static readonly DebugCreateConfiguration = new MenuId('DebugCreateConfiguration'); static readonly EditorContext = new MenuId('EditorContext'); @@ -127,6 +128,7 @@ export class MenuId { static readonly SCMHistoryItemContext = new MenuId('SCMHistoryItemContext'); static readonly SCMHistoryItemHover = new MenuId('SCMHistoryItemHover'); static readonly SCMHistoryItemRefContext = new MenuId('SCMHistoryItemRefContext'); + static readonly SCMQuickDiffDecorations = new MenuId('SCMQuickDiffDecorations'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly SearchActionMenu = new MenuId('SearchActionContext'); @@ -602,7 +604,7 @@ interface IBaseAction2Options extends IAction2CommonOptions { f1?: false; } -interface ICommandPaletteOptions extends IAction2CommonOptions { +export interface ICommandPaletteOptions extends IAction2CommonOptions { /** * The title of the command that will be displayed in the command palette after the category. diff --git a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts index 7384c57f4f4..a67d76690ab 100644 --- a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts +++ b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows.ts @@ -17,6 +17,7 @@ export interface IAuxiliaryWindowsMainService { readonly onDidMaximizeWindow: Event; readonly onDidUnmaximizeWindow: Event; readonly onDidChangeFullScreen: Event<{ window: IAuxiliaryWindow; fullscreen: boolean }>; + readonly onDidChangeAlwaysOnTop: Event<{ window: IAuxiliaryWindow; alwaysOnTop: boolean }>; readonly onDidTriggerSystemContextMenu: Event<{ readonly window: IAuxiliaryWindow; readonly x: number; readonly y: number }>; createWindow(details: HandlerDetails): BrowserWindowConstructorOptions; diff --git a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index 3ef1cb28108..12754b8b00a 100644 --- a/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/code/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -28,6 +28,9 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar private readonly _onDidChangeFullScreen = this._register(new Emitter<{ window: IAuxiliaryWindow; fullscreen: boolean }>()); readonly onDidChangeFullScreen = this._onDidChangeFullScreen.event; + private readonly _onDidChangeAlwaysOnTop = this._register(new Emitter<{ window: IAuxiliaryWindow; alwaysOnTop: boolean }>()); + readonly onDidChangeAlwaysOnTop = this._onDidChangeAlwaysOnTop.event; + private readonly _onDidTriggerSystemContextMenu = this._register(new Emitter<{ window: IAuxiliaryWindow; x: number; y: number }>()); readonly onDidTriggerSystemContextMenu = this._onDidTriggerSystemContextMenu.event; @@ -126,6 +129,9 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar case 'window-native-titlebar': overrides.forceNativeTitlebar = true; break; + case 'window-always-on-top': + overrides.alwaysOnTop = true; + break; } } @@ -148,6 +154,7 @@ export class AuxiliaryWindowsMainService extends Disposable implements IAuxiliar disposables.add(auxiliaryWindow.onDidUnmaximize(() => this._onDidUnmaximizeWindow.fire(auxiliaryWindow))); disposables.add(auxiliaryWindow.onDidEnterFullScreen(() => this._onDidChangeFullScreen.fire({ window: auxiliaryWindow, fullscreen: true }))); disposables.add(auxiliaryWindow.onDidLeaveFullScreen(() => this._onDidChangeFullScreen.fire({ window: auxiliaryWindow, fullscreen: false }))); + disposables.add(auxiliaryWindow.onDidChangeAlwaysOnTop(alwaysOnTop => this._onDidChangeAlwaysOnTop.fire({ window: auxiliaryWindow, alwaysOnTop }))); disposables.add(auxiliaryWindow.onDidTriggerSystemContextMenu(({ x, y }) => this._onDidTriggerSystemContextMenu.fire({ window: auxiliaryWindow, x, y }))); Event.once(auxiliaryWindow.onDidClose)(() => disposables.dispose()); diff --git a/code/src/vs/platform/clipboard/browser/clipboardService.ts b/code/src/vs/platform/clipboard/browser/clipboardService.ts index 787d3dac4f8..2167555b24e 100644 --- a/code/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/code/src/vs/platform/clipboard/browser/clipboardService.ts @@ -45,6 +45,10 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer }, { window: mainWindow, disposables: this._store })); } + triggerPaste(): Promise | undefined { + return undefined; + } + async readImage(): Promise { try { const clipboardItems = await navigator.clipboard.read(); diff --git a/code/src/vs/platform/clipboard/common/clipboardService.ts b/code/src/vs/platform/clipboard/common/clipboardService.ts index cef3ff82105..be69b5219aa 100644 --- a/code/src/vs/platform/clipboard/common/clipboardService.ts +++ b/code/src/vs/platform/clipboard/common/clipboardService.ts @@ -12,6 +12,11 @@ export interface IClipboardService { readonly _serviceBrand: undefined; + /** + * Trigger the paste. Returns undefined if the paste was not triggered or a promise that resolves on paste end. + */ + triggerPaste(targetWindowId: number): Promise | undefined; + /** * Writes text to the system clipboard. */ diff --git a/code/src/vs/platform/clipboard/test/common/testClipboardService.ts b/code/src/vs/platform/clipboard/test/common/testClipboardService.ts index 23a20010dd3..261755d0d61 100644 --- a/code/src/vs/platform/clipboard/test/common/testClipboardService.ts +++ b/code/src/vs/platform/clipboard/test/common/testClipboardService.ts @@ -15,6 +15,10 @@ export class TestClipboardService implements IClipboardService { private text: string | undefined = undefined; + triggerPaste(): Promise | undefined { + return Promise.resolve(); + } + async writeText(text: string, type?: string): Promise { this.text = text; } diff --git a/code/src/vs/platform/configuration/common/configurationRegistry.ts b/code/src/vs/platform/configuration/common/configurationRegistry.ts index bd8a8f2065f..4bfc7cbf6b8 100644 --- a/code/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/code/src/vs/platform/configuration/common/configurationRegistry.ts @@ -13,6 +13,7 @@ import { getLanguageTagSettingPlainKey } from './configuration.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../jsonschemas/common/jsonContributionRegistry.js'; import { Registry } from '../../registry/common/platform.js'; import { IPolicy, PolicyName } from '../../../base/common/policy.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; export enum EditPresentationTypes { Multiline = 'multilineText', @@ -272,7 +273,7 @@ export const configurationDefaultsSchemaId = 'vscode://schemas/settings/configur const contributionRegistry = Registry.as(JSONExtensions.JSONContribution); -class ConfigurationRegistry implements IConfigurationRegistry { +class ConfigurationRegistry extends Disposable implements IConfigurationRegistry { private readonly registeredConfigurationDefaults: IConfigurationDefaults[] = []; private readonly configurationDefaultsOverrides: Map; @@ -284,13 +285,14 @@ class ConfigurationRegistry implements IConfigurationRegistry { private readonly resourceLanguageSettingsSchema: IJSONSchema; private readonly overrideIdentifiers = new Set(); - private readonly _onDidSchemaChange = new Emitter(); + private readonly _onDidSchemaChange = this._register(new Emitter()); readonly onDidSchemaChange: Event = this._onDidSchemaChange.event; - private readonly _onDidUpdateConfiguration = new Emitter<{ properties: ReadonlySet; defaultsOverrides?: boolean }>(); + private readonly _onDidUpdateConfiguration = this._register(new Emitter<{ properties: ReadonlySet; defaultsOverrides?: boolean }>()); readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event; constructor() { + super(); this.configurationDefaultsOverrides = new Map(); this.defaultLanguageConfigurationOverridesNode = { id: 'defaultOverrides', diff --git a/code/src/vs/platform/configuration/common/configurations.ts b/code/src/vs/platform/configuration/common/configurations.ts index b3749c2f709..2dea3c6ce98 100644 --- a/code/src/vs/platform/configuration/common/configurations.ts +++ b/code/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/code/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/code/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index 308b1236aba..ac30e8b5050 100644 --- a/code/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/code/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -66,6 +66,14 @@ suite('PolicyConfiguration', () => { minimumVersion: '1.0.0', } }, + 'policy.booleanSetting': { + 'type': 'boolean', + 'default': true, + policy: { + name: 'PolicyBooleanSetting', + minimumVersion: '1.0.0', + } + }, 'policy.internalSetting': { 'type': 'string', 'default': 'defaultInternalValue', @@ -159,6 +167,24 @@ suite('PolicyConfiguration', () => { assert.deepStrictEqual(acutal.getValue('policy.arraySetting'), [1]); }); + test('initialize: with boolean type policy as false', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicyBooleanSetting': false }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.getValue('policy.booleanSetting'), false); + }); + + test('initialize: with boolean type policy as true', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicyBooleanSetting': true }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.getValue('policy.booleanSetting'), true); + }); + test('initialize: with object type policy ignores policy if value is not valid', async () => { await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicyObjectSetting': '{"a": "b", "hello": }' }))); diff --git a/code/src/vs/platform/cssDev/node/cssDevService.ts b/code/src/vs/platform/cssDev/node/cssDevService.ts index 5aa1efa6e39..5bbc93b3b28 100644 --- a/code/src/vs/platform/cssDev/node/cssDevService.ts +++ b/code/src/vs/platform/cssDev/node/cssDevService.ts @@ -49,21 +49,23 @@ export class CSSDevelopmentService implements ICSSDevelopmentService { const sw = StopWatch.create(); - const chunks: string[][] = []; - const decoder = new TextDecoder(); + const chunks: Buffer[] = []; const basePath = FileAccess.asFileUri('').fsPath; const process = spawn(rg.rgPath, ['-g', '**/*.css', '--files', '--no-ignore', basePath], {}); process.stdout.on('data', data => { - const chunk = decoder.decode(data, { stream: true }); - chunks.push(chunk.split('\n').filter(Boolean)); + chunks.push(data); }); process.on('error', err => { this.logService.error('[CSS_DEV] FAILED to compute CSS data', err); resolve([]); }); process.on('close', () => { - const result = chunks.flat().map(path => relative(basePath, path).replace(/\\/g, '/')).filter(Boolean).sort(); + const data = Buffer.concat(chunks).toString('utf8'); + const result = data.split('\n').filter(Boolean).map(path => relative(basePath, path).replace(/\\/g, '/')).filter(Boolean).sort(); + if (result.some(path => path.indexOf('vs/') !== 0)) { + this.logService.error(`[CSS_DEV] Detected invalid paths in css modules, raw output: ${data}`); + } resolve(result); this.logService.info(`[CSS_DEV] DONE, ${result.length} css modules (${Math.round(sw.elapsed())}ms)`); }); diff --git a/code/src/vs/platform/dialogs/common/dialogs.ts b/code/src/vs/platform/dialogs/common/dialogs.ts index bb9270b9c28..b0420f5c5a8 100644 --- a/code/src/vs/platform/dialogs/common/dialogs.ts +++ b/code/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/code/src/vs/platform/dnd/browser/dnd.ts b/code/src/vs/platform/dnd/browser/dnd.ts index f00c4d08a73..d59b32bd10d 100644 --- a/code/src/vs/platform/dnd/browser/dnd.ts +++ b/code/src/vs/platform/dnd/browser/dnd.ts @@ -33,6 +33,7 @@ export const CodeDataTransfers = { FILES: 'CodeFiles', SYMBOLS: 'application/vnd.code.symbols', MARKERS: 'application/vnd.code.diagnostics', + NOTEBOOK_CELL_OUTPUT: 'notebook-cell-output', }; export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { @@ -416,6 +417,10 @@ export interface DocumentSymbolTransferData { kind: number; } +export interface NotebookCellOutputTransferData { + outputId: string; +} + function setDataAsJSON(e: DragEvent, kind: string, data: unknown) { e.dataTransfer?.setData(kind, JSON.stringify(data)); } @@ -451,6 +456,10 @@ export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragE setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData); } +export function extractNotebookCellOutputDropData(e: DragEvent): NotebookCellOutputTransferData | undefined { + return getDataAsJSON(e, CodeDataTransfers.NOTEBOOK_CELL_OUTPUT, undefined); +} + /** * A helper to get access to Electrons `webUtils.getPathForFile` function * in a safe way without crashing the application when running in the web. diff --git a/code/src/vs/platform/editor/common/editor.ts b/code/src/vs/platform/editor/common/editor.ts index 0138721fb1d..7c856237f67 100644 --- a/code/src/vs/platform/editor/common/editor.ts +++ b/code/src/vs/platform/editor/common/editor.ts @@ -250,11 +250,6 @@ export interface IEditorOptions { */ inactive?: boolean; - /** - * Will not show an error in case opening the editor fails and thus allows to show a custom error - * message as needed. By default, an error will be presented as notification if opening was not possible. - */ - /** * In case of an error opening the editor, will not present this error to the user (e.g. by showing * a generic placeholder in the editor area). So it is up to the caller to provide error information @@ -303,6 +298,14 @@ export interface IEditorOptions { * not turn transient. */ transient?: boolean; + + /** + * A hint that the editor should have compact chrome when showing if possible. + * + * Note: this currently is only working if AUX_GROUP is specified as target to + * open the editor in a floating window. + */ + compact?: boolean; } export interface ITextEditorSelection { diff --git a/code/src/vs/platform/environment/node/argv.ts b/code/src/vs/platform/environment/node/argv.ts index 0025e4b5973..35599a622de 100644 --- a/code/src/vs/platform/environment/node/argv.ts +++ b/code/src/vs/platform/environment/node/argv.ts @@ -104,7 +104,7 @@ export const OPTIONS: OptionDescriptions> = { 'update-extensions': { type: 'boolean', cat: 'e', description: localize('updateExtensions', "Update the installed extensions.") }, 'enable-proposed-api': { type: 'string[]', allowEmptyValue: true, cat: 'e', args: 'ext-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, - 'add-mcp': { type: 'string[]', cat: 'o', args: 'json', description: localize('addMcp', "Adds a Model Context Protocol server definition to the user profile, or workspace or folder when used with --mcp-workspace. Accepts JSON input in the form '{\"name\":\"server-name\",\"command\":...}'") }, + 'add-mcp': { type: 'string[]', cat: 'o', args: 'json', description: localize('addMcp', "Adds a Model Context Protocol server definition to the user profile. Accepts JSON input in the form '{\"name\":\"server-name\",\"command\":...}'") }, 'version': { type: 'boolean', cat: 't', alias: 'v', description: localize('version', "Print version.") }, 'verbose': { type: 'boolean', cat: 't', global: true, description: localize('verbose', "Print verbose output (implies --wait).") }, diff --git a/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index f0f8be2593a..06e1a28cf02 100644 --- a/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/code/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -62,11 +62,14 @@ export abstract class CommontExtensionManagementService extends Disposable imple _serviceBrand: undefined; + readonly preferPreReleases: boolean; + constructor( @IProductService protected readonly productService: IProductService, @IAllowedExtensionsService protected readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(); + this.preferPreReleases = this.productService.quality !== 'stable'; } async canInstall(extension: IGalleryExtension): Promise { @@ -98,7 +101,7 @@ export abstract class CommontExtensionManagementService extends Disposable imple abstract installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise; abstract uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; abstract uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise; - abstract toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; + abstract toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; abstract getExtensionsControlManifest(): Promise; abstract resetPinnedStateForAllUserExtensions(pinned: boolean): Promise; abstract registerParticipant(pariticipant: IExtensionManagementParticipant): void; @@ -204,7 +207,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio return this.uninstallExtensions([{ extension, options }]); } - async toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { + async toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { if (isApplicationScopedExtension(extension.manifest) || extension.isBuiltin) { return extension; } @@ -341,9 +344,16 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, !!task.options.installPreReleaseVersion, task.options.productVersion); + let preferPreRelease = this.preferPreReleases; + if (task.options.installPreReleaseVersion) { + preferPreRelease = true; + } else if (!URI.isUri(task.source) && task.source.hasPreReleaseVersion) { + // Explicitly asked to install the release version + preferPreRelease = false; + } + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, preferPreRelease, task.options.productVersion); const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); - const options: InstallExtensionTaskOptions = { ...task.options, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; + const options: InstallExtensionTaskOptions = { ...task.options, pinned: false, installGivenVersion: false, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { const existing = installed.find(e => areSameExtensions(e.identifier, gallery.identifier)); // Skip if the extension is already installed and has the same application scope @@ -579,7 +589,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio throw error; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, preferPreRelease: boolean, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } @@ -603,7 +613,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio // filter out known extensions const ids = dependenciesAndPackExtensions.filter(id => knownIdentifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id }))); if (ids.length) { - const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: installPreRelease })), CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: preferPreRelease })), CancellationToken.None); for (const galleryExtension of galleryExtensions) { if (knownIdentifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) { continue; @@ -611,7 +621,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, preferPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); @@ -661,7 +671,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi); } /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ - if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { + if (!installPreRelease && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); } throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); diff --git a/code/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts b/code/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts index 14e529e97e1..0999bc27635 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts @@ -15,7 +15,7 @@ export const enum ExtensionGalleryResourceType { ExtensionDetailsViewUri = 'ExtensionDetailsViewUriTemplate', ExtensionRatingViewUri = 'ExtensionRatingViewUriTemplate', ExtensionResourceUri = 'ExtensionResourceUriTemplate', - ReportIssueUri = 'ReportIssueUri', + ContactSupportUri = 'ContactSupportUri', } export const enum Flag { @@ -55,7 +55,12 @@ export interface IExtensionGalleryManifest { readonly flags?: readonly ExtensionQueryCapabilityValue[]; }; readonly signing?: { - readonly allRepositorySigned: boolean; + readonly allPublicRepositorySigned: boolean; + readonly allPrivateRepositorySigned?: boolean; + }; + readonly extensions?: { + readonly includePublicExtensions?: boolean; + readonly includePrivateExtensions?: boolean; }; }; } @@ -70,10 +75,11 @@ export interface IExtensionGalleryManifestService { getExtensionGalleryManifest(): Promise; } -export function getExtensionGalleryManifestResourceUri(manifest: IExtensionGalleryManifest, type: ExtensionGalleryResourceType, version?: string): string | undefined { +export function getExtensionGalleryManifestResourceUri(manifest: IExtensionGalleryManifest, type: string): string | undefined { + const [name, version] = type.split('/'); for (const resource of manifest.resources) { const [r, v] = resource.type.split('/'); - if (r !== type) { + if (r !== name) { continue; } if (!version || v === version) { diff --git a/code/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts b/code/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts index ebbb18d3a36..70e34e808be 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts @@ -68,11 +68,11 @@ export class ExtensionGalleryManifestService extends Disposable implements IExte if (extensionsGallery.itemUrl) { resources.push({ - id: `${extensionsGallery.itemUrl}/?itemName={publisher}.{name}`, + id: `${extensionsGallery.itemUrl}?itemName={publisher}.{name}`, type: ExtensionGalleryResourceType.ExtensionDetailsViewUri }); resources.push({ - id: `${extensionsGallery.itemUrl}/?itemName={publisher}.{name}&ssr=false#review-details`, + id: `${extensionsGallery.itemUrl}?itemName={publisher}.{name}&ssr=false#review-details`, type: ExtensionGalleryResourceType.ExtensionRatingViewUri }); } @@ -223,7 +223,7 @@ export class ExtensionGalleryManifestService extends Disposable implements IExte flags, }, signing: { - allRepositorySigned: true, + allPublicRepositorySigned: true, } } }; diff --git a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index a0afbe7f79f..7dbac890425 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -5,6 +5,7 @@ import { distinct } from '../../../base/common/arrays.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import * as semver from '../../../base/common/semver/semver.js'; import { IStringDictionary } from '../../../base/common/collections.js'; import { CancellationError, getErrorMessage, isCancellationError } from '../../../base/common/errors.js'; import { IPager } from '../../../base/common/paging.js'; @@ -15,7 +16,7 @@ import { URI } from '../../../base/common/uri.js'; import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApiConfigKey, IAllowedExtensionsService, EXTENSION_IDENTIFIER_REGEX, SortBy, FilterType } from './extensionManagement.js'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApiConfigKey, IAllowedExtensionsService, EXTENSION_IDENTIFIER_REGEX, SortBy, FilterType, MaliciousExtensionInfo } from './extensionManagement.js'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from './extensionManagementUtil.js'; import { IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { areApiProposalsCompatible, isEngineValid } from '../../extensions/common/extensionValidator.js'; @@ -30,6 +31,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'; @@ -68,6 +70,7 @@ interface IRawGalleryExtensionPublisher { readonly publisherName: string; readonly domain?: string | null; readonly isDomainVerified?: boolean; + readonly linkType?: string; } interface IRawGalleryExtension { @@ -84,6 +87,8 @@ interface IRawGalleryExtension { readonly lastUpdated: string; readonly categories: string[] | undefined; readonly flags: string; + readonly linkType?: string; + readonly ratingLinkType?: string; } interface IRawGalleryExtensionsResult { @@ -198,9 +203,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 = { @@ -349,6 +354,14 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function hasPreReleaseForExtension(id: string, productService: IProductService): boolean | undefined { + return productService.extensionProperties?.[id.toLowerCase()]?.hasPrereleaseVersion; +} + +function getExcludeVersionRangeForExtension(id: string, productService: IProductService): string | undefined { + return productService.extensionProperties?.[id.toLowerCase()]?.excludeVersionRange; +} + function isPrivateExtension(version: IRawGalleryExtensionVersion): boolean { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Private) : []; return values.length > 0 && values[0].value === 'true'; @@ -441,7 +454,7 @@ function setTelemetry(extension: IGalleryExtension, index: number, querySource?: extension.telemetryData = { index, querySource, queryActivityId: extension.queryContext?.[SEARCH_ACTIVITY_HEADER_NAME] }; } -function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], extensionGalleryManifest: IExtensionGalleryManifest, queryContext?: IStringDictionary): IGalleryExtension { +function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], extensionGalleryManifest: IExtensionGalleryManifest, productService: IProductService, queryContext?: IStringDictionary): IGalleryExtension { const latestVersion = galleryExtension.versions[0]; const assets: IGalleryExtensionAssets = { manifest: getVersionAsset(version, AssetType.Manifest), @@ -455,14 +468,15 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller coreTranslations: getCoreTranslationAssets(version) }; - const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionDetailsViewUri); - const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.PublisherViewUri); - const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionRatingViewUri); + const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.linkType ?? ExtensionGalleryResourceType.ExtensionDetailsViewUri); + const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.publisher.linkType ?? ExtensionGalleryResourceType.PublisherViewUri); + const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.ratingLinkType ?? ExtensionGalleryResourceType.ExtensionRatingViewUri); + const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName); return { type: 'gallery', identifier: { - id: getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName), + id, uuid: galleryExtension.extensionId }, name: galleryExtension.extensionName, @@ -493,7 +507,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller isPreReleaseVersion: isPreReleaseVersion(version), executesCode: executesCode(version) }, - hasPreReleaseVersion: isPreReleaseVersion(latestVersion), + hasPreReleaseVersion: hasPreReleaseForExtension(id, productService) ?? isPreReleaseVersion(latestVersion), hasReleaseVersion: true, private: isPrivateExtension(latestVersion), preview: getIsPreview(galleryExtension.flags), @@ -508,6 +522,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller interface IRawExtensionsControlManifest { malicious: string[]; + learnMoreLinks?: IStringDictionary; migrateToPreRelease?: IStringDictionary<{ id: string; displayName: string; @@ -524,7 +539,6 @@ interface IRawExtensionsControlManifest { additionalInfo?: string; }>; search?: ISearchPrefferedResults[]; - extensionsEnabledWithPreRelease?: string[]; } export abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -818,7 +832,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle }, allTargetPlatforms); if (rawGalleryExtensionVersion) { - return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest); + return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService); } return null; @@ -849,92 +863,124 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { - if (this.allowedExtensionsService.isAllowed(extension) !== true) { + return this.isValidVersion( + { + id: extension.identifier.id, + version: extension.version, + isPreReleaseVersion: extension.properties.isPreReleaseVersion, + targetPlatform: extension.properties.targetPlatform, + manifestAsset: extension.assets.manifest, + engine: extension.properties.engine, + enabledApiProposals: extension.properties.enabledApiProposals + }, + { + targetPlatform, + compatible: true, + productVersion, + version: includePreRelease ? VersionKind.Latest : VersionKind.Release + }, + extension.publisherDisplayName, + extension.allTargetPlatforms + ); + } + + private async isValidVersion( + extension: { id: string; version: string; isPreReleaseVersion: boolean; targetPlatform: TargetPlatform; manifestAsset: IGalleryExtensionAsset | null; engine: string | undefined; enabledApiProposals: string[] | undefined }, + { targetPlatform, compatible, productVersion, version }: ExtensionVersionCriteria, + publisherDisplayName: string, + allTargetPlatforms: TargetPlatform[] + ): Promise { + + const hasPreRelease = hasPreReleaseForExtension(extension.id, this.productService); + const excludeVersionRange = getExcludeVersionRangeForExtension(extension.id, this.productService); + + if (extension.isPreReleaseVersion && hasPreRelease === false /* Skip if hasPreRelease is not defined for this extension */) { return false; } - if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { + if (excludeVersionRange && semver.satisfies(extension.version, excludeVersionRange)) { return false; } - if (!includePreRelease && extension.properties.isPreReleaseVersion) { - // Pre-releases are not allowed when include pre-release flag is not set - return false; + // Specific version + if (isString(version)) { + if (extension.version !== version) { + return false; + } } - let engine = extension.properties.engine; - if (!engine) { - const manifest = await this.getManifest(extension, CancellationToken.None); - if (!manifest) { - throw new Error('Manifest was not found'); + // Prerelease or release version kind + else if (version === VersionKind.Release || version === VersionKind.Prerelease) { + if (extension.isPreReleaseVersion !== (version === VersionKind.Prerelease)) { + return false; } - engine = manifest.engines.vscode; } - if (!isEngineValid(engine, productVersion.version, productVersion.date)) { + if (!isTargetPlatformCompatible(extension.targetPlatform, allTargetPlatforms, targetPlatform)) { return false; } - if (!this.areApiProposalsCompatible(extension.identifier, extension.properties.enabledApiProposals)) { - return false; + if (compatible) { + if (this.allowedExtensionsService.isAllowed({ id: extension.id, publisherDisplayName, version: extension.version, prerelease: extension.isPreReleaseVersion, targetPlatform: extension.targetPlatform }) !== true) { + return false; + } + + if (!this.areApiProposalsCompatible(extension.id, extension.enabledApiProposals)) { + return false; + } + + if (!(await this.isEngineValid(extension.id, extension.version, extension.engine, extension.manifestAsset, productVersion))) { + return false; + } } return true; } - private areApiProposalsCompatible(extensionIdentifier: IExtensionIdentifier, enabledApiProposals: string[] | undefined): boolean { + private areApiProposalsCompatible(extensionId: string, enabledApiProposals: string[] | undefined): boolean { if (!enabledApiProposals) { return true; } - if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionIdentifier.id.toLowerCase())) { + if (!this.extensionsEnabledWithApiProposalVersion.includes(extensionId.toLowerCase())) { return true; } return areApiProposalsCompatible(enabledApiProposals); } - private async isValidVersion( - extension: string, - rawGalleryExtensionVersion: IRawGalleryExtensionVersion, - { targetPlatform, compatible, productVersion, version }: ExtensionVersionCriteria, - publisherDisplayName: string, - allTargetPlatforms: TargetPlatform[] - ): Promise { - - // Specific version - if (isString(version)) { - if (rawGalleryExtensionVersion.version !== version) { - return false; - } - } - - // Prerelease or release version kind - else if (version === VersionKind.Release || version === VersionKind.Prerelease) { - if (isPreReleaseVersion(rawGalleryExtensionVersion) !== (version === VersionKind.Prerelease)) { - return false; - } - } - - const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); - if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { - return false; - } - - if (compatible) { - if (this.allowedExtensionsService.isAllowed({ id: extension, publisherDisplayName, version: rawGalleryExtensionVersion.version, prerelease: isPreReleaseVersion(rawGalleryExtensionVersion), targetPlatform: targetPlatformForExtension }) !== true) { + private async isEngineValid(extensionId: string, version: string, engine: string | undefined, manifestAsset: IGalleryExtensionAsset | null, productVersion: IProductVersion): Promise { + if (!engine) { + if (!manifestAsset) { + this.logService.error(`Missing engine and manifest asset for the extension ${extensionId} with version ${version}`); return false; } try { - const engine = await this.getEngine(extension, rawGalleryExtensionVersion); - if (!isEngineValid(engine, productVersion.version, productVersion.date)) { + type GalleryServiceEngineFallbackClassification = { + owner: 'sandy081'; + comment: 'Fallback request when engine is not found in properties of an extension version'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' }; + }; + type GalleryServiceEngineFallbackEvent = { + extension: string; + extensionVersion: string; + }; + this.telemetryService.publicLog2('galleryService:engineFallback', { extension: extensionId, extensionVersion: version }); + + const headers = { 'Accept-Encoding': 'gzip' }; + const context = await this.getAsset(extensionId, manifestAsset, AssetType.Manifest, version, { headers }); + const manifest = await asJson(context); + if (!manifest) { + this.logService.error(`Manifest was not found for the extension ${extensionId} with version ${version}`); return false; } + engine = manifest.engines.vscode; } catch (error) { - this.logService.error(`Error while getting the engine for the version ${rawGalleryExtensionVersion.version}.`, getErrorMessage(error)); + this.logService.error(`Error while getting the engine for the version ${version}.`, getErrorMessage(error)); return false; } } - return true; + return isEngineValid(engine, productVersion.version, productVersion.date); } async query(options: IQueryOptions, token: CancellationToken): Promise> { @@ -1074,7 +1120,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle allTargetPlatforms ); if (rawGalleryExtensionVersion) { - extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, context)); + extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context)); } } return { extensions, total }; @@ -1108,7 +1154,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle }, allTargetPlatforms ); - const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, context) : null; + const extension = rawGalleryExtensionVersion ? toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context) : null; if (!extension /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or @@ -1208,7 +1254,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle allTargetPlatforms ); if (rawGalleryExtensionVersion) { - extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, context)); + extensions.push(toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, extensionGalleryManifest, this.productService, context)); } } @@ -1228,15 +1274,19 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle for (let index = 0; index < rawGalleryExtensionVersions.length; index++) { const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index]; if (await this.isValidVersion( - extensionIdentifier.id, - rawGalleryExtensionVersion, + { + id: extensionIdentifier.id, + version: rawGalleryExtensionVersion.version, + isPreReleaseVersion: isPreReleaseVersion(rawGalleryExtensionVersion), + targetPlatform: getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), + engine: getEngine(rawGalleryExtensionVersion), + manifestAsset: getVersionAsset(rawGalleryExtensionVersion, AssetType.Manifest), + enabledApiProposals: getEnabledApiProposals(rawGalleryExtensionVersion) + }, criteria, rawGalleryExtension.publisher.displayName, allTargetPlatforms) ) { - if (criteria.compatible && !this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(rawGalleryExtensionVersion))) { - continue; - } return rawGalleryExtensionVersion; } if (version && rawGalleryExtensionVersion.version === version) { @@ -1379,9 +1429,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 +1504,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, @@ -1581,16 +1632,6 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return null; } - private async getManifestFromRawExtensionVersion(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion, token: CancellationToken): Promise { - const manifestAsset = getVersionAsset(rawExtensionVersion, AssetType.Manifest); - if (!manifestAsset) { - throw new Error('Manifest was not found'); - } - const headers = { 'Accept-Encoding': 'gzip' }; - const context = await this.getAsset(extension, manifestAsset, AssetType.Manifest, rawExtensionVersion.version, { headers }); - return await asJson(context); - } - async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise { const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0]; if (asset) { @@ -1636,14 +1677,21 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle return []; } - const validVersions: IRawGalleryExtensionVersion[] = []; + const compatibleVersions: IRawGalleryExtensionVersion[] = []; const productVersion = { version: this.productService.version, date: this.productService.date }; await Promise.all(galleryExtensions[0].versions.map(async (version) => { try { if ( (await this.isValidVersion( - extensionIdentifier.id, - version, + { + id: extensionIdentifier.id, + version: version.version, + isPreReleaseVersion: isPreReleaseVersion(version), + targetPlatform: getTargetPlatformForExtensionVersion(version), + engine: getEngine(version), + manifestAsset: getVersionAsset(version, AssetType.Manifest), + enabledApiProposals: getEnabledApiProposals(version) + }, { compatible: true, productVersion, @@ -1652,16 +1700,15 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle }, galleryExtensions[0].publisher.displayName, allTargetPlatforms)) - && this.areApiProposalsCompatible(extensionIdentifier, getEnabledApiProposals(version)) ) { - validVersions.push(version); + compatibleVersions.push(version); } } catch (error) { /* Ignore error and skip version */ } })); const result: IGalleryExtensionVersion[] = []; const seen = new Set(); - for (const version of sortExtensionVersions(validVersions, targetPlatform)) { + for (const version of sortExtensionVersions(compatibleVersions, targetPlatform)) { if (!seen.has(version.version)) { seen.add(version.version); result.push({ version: version.version, date: version.lastUpdated, isPreReleaseVersion: isPreReleaseVersion(version) }); @@ -1711,9 +1758,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, @@ -1730,29 +1777,6 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } } - private async getEngine(extension: string, rawExtensionVersion: IRawGalleryExtensionVersion): Promise { - let engine = getEngine(rawExtensionVersion); - if (!engine) { - type GalleryServiceEngineFallbackClassification = { - owner: 'sandy081'; - comment: 'Fallback request when engine is not found in properties of an extension version'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension name' }; - extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'version' }; - }; - type GalleryServiceEngineFallbackEvent = { - extension: string; - extensionVersion: string; - }; - this.telemetryService.publicLog2('galleryService:engineFallback', { extension, extensionVersion: rawExtensionVersion.version }); - const manifest = await this.getManifestFromRawExtensionVersion(extension, rawExtensionVersion, CancellationToken.None); - if (!manifest) { - throw new Error('Manifest was not found'); - } - engine = manifest.engines.vscode; - } - return engine; - } - async getExtensionsControlManifest(): Promise { if (!this.isEnabled()) { throw new Error('No extension gallery service configured.'); @@ -1773,17 +1797,16 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } const result = await asJson(context); - const malicious: Array = []; + const malicious: Array = []; const deprecated: IStringDictionary = {}; const search: ISearchPrefferedResults[] = []; - const extensionsEnabledWithPreRelease: string[] = []; if (result) { for (const id of result.malicious) { - if (EXTENSION_IDENTIFIER_REGEX.test(id)) { - malicious.push({ id }); - } else { - malicious.push(id); + if (!isString(id)) { + continue; } + const publisherOrExtension = EXTENSION_IDENTIFIER_REGEX.test(id) ? { id } : id; + malicious.push({ extensionOrPublisher: publisherOrExtension, learnMoreLink: result.learnMoreLinks?.[id] }); } if (result.migrateToPreRelease) { for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) { @@ -1812,14 +1835,9 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle search.push(s); } } - if (Array.isArray(result.extensionsEnabledWithPreRelease)) { - for (const id of result.extensionsEnabledWithPreRelease) { - extensionsEnabledWithPreRelease.push(id.toLowerCase()); - } - } } - return { malicious, deprecated, search, extensionsEnabledWithPreRelease }; + return { malicious, deprecated, search }; } } diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts index 72de79bab6a..109607b695c 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -10,10 +10,13 @@ import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IPager } from '../../../base/common/paging.js'; import { Platform } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; -import { localize2 } from '../../../nls.js'; +import { localize, localize2 } from '../../../nls.js'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { FileOperationError, FileOperationResult, IFileService, IFileStat } from '../../files/common/files.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { Registry } from '../../registry/common/platform.js'; +import { IExtensionGalleryManifest } from './extensionGalleryManifest.js'; export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); @@ -345,11 +348,15 @@ export interface ISearchPrefferedResults { readonly preferredResults?: string[]; } +export type MaliciousExtensionInfo = { + readonly extensionOrPublisher: IExtensionIdentifier | string; + readonly learnMoreLink?: string; +}; + export interface IExtensionsControlManifest { - readonly malicious: ReadonlyArray; + readonly malicious: ReadonlyArray; readonly deprecated: IStringDictionary; readonly search: ISearchPrefferedResults[]; - readonly extensionsEnabledWithPreRelease?: string[]; } export const enum InstallOperation { @@ -589,6 +596,8 @@ export const IExtensionManagementService = createDecorator; onDidInstallExtensions: Event; onUninstallExtension: Event; @@ -605,7 +614,7 @@ export interface IExtensionManagementService { installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise; - toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; + toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; @@ -696,3 +705,86 @@ export const ExtensionsLocalizedLabel = localize2('extensions', "Extensions"); export const PreferencesLocalizedLabel = localize2('preferences', 'Preferences'); export const UseUnpkgResourceApiConfigKey = 'extensions.gallery.useUnpkgResourceApi'; export const AllowedExtensionsConfigKey = 'extensions.allowed'; +export const VerifyExtensionSignatureConfigKey = 'extensions.verifySignature'; + +Registry.as(Extensions.Configuration) + .registerConfiguration({ + id: 'extensions', + order: 30, + title: localize('extensionsConfigurationTitle', "Extensions"), + type: 'object', + properties: { + [AllowedExtensionsConfigKey]: { + // Note: Type is set only to object because to support policies generation during build time, where single type is expected. + type: 'object', + markdownDescription: localize('extensions.allowed', "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. For more information on how to configure this setting, please visit the [Configure Allowed Extensions](https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions) section."), + default: '*', + defaultSnippets: [{ + body: {}, + description: localize('extensions.allowed.none', "No extensions are allowed."), + }, { + body: { + '*': true + }, + description: localize('extensions.allowed.all', "All extensions are allowed."), + }], + scope: ConfigurationScope.APPLICATION, + policy: { + name: 'AllowedExtensions', + minimumVersion: '1.96', + description: localize('extensions.allowed.policy', "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. More information: https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions"), + }, + additionalProperties: false, + patternProperties: { + '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + anyOf: [ + { + type: ['boolean', 'string'], + enum: [true, false, 'stable'], + description: localize('extensions.allow.description', "Allow or disallow the extension."), + enumDescriptions: [ + localize('extensions.allowed.enable.desc', "Extension is allowed."), + localize('extensions.allowed.disable.desc', "Extension is not allowed."), + localize('extensions.allowed.disable.stable.desc', "Allow only stable versions of the extension."), + ], + }, + { + type: 'array', + items: { + type: 'string', + }, + description: localize('extensions.allow.version.description', "Allow or disallow specific versions of the extension. To specifcy a platform specific version, use the format `platform@1.2.3`, e.g. `win32-x64@1.2.3`. Supported platforms are `win32-x64`, `win32-arm64`, `linux-x64`, `linux-arm64`, `linux-armhf`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`"), + }, + ] + }, + '([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + type: ['boolean', 'string'], + enum: [true, false, 'stable'], + description: localize('extension.publisher.allow.description', "Allow or disallow all extensions from the publisher."), + enumDescriptions: [ + localize('extensions.publisher.allowed.enable.desc', "All extensions from the publisher are allowed."), + localize('extensions.publisher.allowed.disable.desc', "All extensions from the publisher are not allowed."), + localize('extensions.publisher.allowed.disable.stable.desc', "Allow only stable versions of the extensions from the publisher."), + ], + }, + '\\*': { + type: 'boolean', + enum: [true, false], + description: localize('extensions.allow.all.description', "Allow or disallow all extensions."), + enumDescriptions: [ + localize('extensions.allow.all.enable', "Allow all extensions."), + localize('extensions.allow.all.disable', "Disallow all extensions.") + ], + } + } + } + } + }); + +export function shouldRequireRepositorySignatureFor(isPrivate: boolean, galleryManifest: IExtensionGalleryManifest | null): boolean { + if (isPrivate) { + return galleryManifest?.capabilities.signing?.allPrivateRepositorySigned === true; + } + return galleryManifest?.capabilities.signing?.allPublicRepositorySigned === true; +} + diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index e20ce088e42..4e30ac2c3a3 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -148,8 +148,8 @@ export class ExtensionManagementChannel implements IServerChannel { const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer), args[2]); return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); } - case 'toggleAppliationScope': { - const extension = await this.service.toggleAppliationScope(transformIncomingExtension(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer)); + case 'toggleApplicationScope': { + const extension = await this.service.toggleApplicationScope(transformIncomingExtension(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer)); return transformOutgoingExtension(extension, uriTransformer); } case 'copyExtensions': { @@ -310,8 +310,8 @@ export class ExtensionManagementChannelClient extends CommontExtensionManagement return this.channel.call('resetPinnedStateForAllUserExtensions', [pinned]); } - toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { - return this.channel.call('toggleAppliationScope', [local, fromProfileLocation]) + toggleApplicationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { + return this.channel.call('toggleApplicationScope', [local, fromProfileLocation]) .then(extension => transformIncomingExtension(extension, null)); } diff --git a/code/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/code/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index ece6b624163..c7d04e5ebc5 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareIgnoreCase } from '../../../base/common/strings.js'; -import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, getTargetPlatform } from './extensionManagement.js'; +import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, MaliciousExtensionInfo, getTargetPlatform } from './extensionManagement.js'; import { ExtensionIdentifier, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; import { IFileService } from '../../files/common/files.js'; import { isLinux, platform } from '../../../base/common/platform.js'; @@ -194,15 +194,19 @@ async function isAlpineLinux(fileService: IFileService, logService: ILogService) export async function computeTargetPlatform(fileService: IFileService, logService: ILogService): Promise { const alpineLinux = await isAlpineLinux(fileService, logService); const targetPlatform = getTargetPlatform(alpineLinux ? 'alpine' : platform, arch); - logService.debug('ComputeTargetPlatform:', targetPlatform); + logService.info('ComputeTargetPlatform:', targetPlatform); return targetPlatform; } -export function isMalicious(identifier: IExtensionIdentifier, malicious: ReadonlyArray): boolean { - return malicious.some(publisherOrIdentifier => { - if (isString(publisherOrIdentifier)) { - return compareIgnoreCase(identifier.id.split('.')[0], publisherOrIdentifier) === 0; +export function isMalicious(identifier: IExtensionIdentifier, malicious: ReadonlyArray): boolean { + return findMatchingMaliciousEntry(identifier, malicious) !== undefined; +} + +export function findMatchingMaliciousEntry(identifier: IExtensionIdentifier, malicious: ReadonlyArray): MaliciousExtensionInfo | undefined { + return malicious.find(({ extensionOrPublisher }) => { + if (isString(extensionOrPublisher)) { + return compareIgnoreCase(identifier.id.split('.')[0], extensionOrPublisher) === 0; } - return areSameExtensions(identifier, publisherOrIdentifier); + return areSameExtensions(identifier, extensionOrPublisher); }); } diff --git a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 16ba453fd05..4e920ab4ee2 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -177,7 +177,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; for (const profileExtension of profileExtensions) { - const extension = extensions.find(([e]) => areSameExtensions(e.identifier, profileExtension.identifier) && e.manifest.version === profileExtension.version); + const extension = extensions.find(([e]) => areSameExtensions({ id: e.identifier.id }, { id: profileExtension.identifier.id }) && e.manifest.version === profileExtension.version); if (extension) { profileExtension.metadata = { ...profileExtension.metadata, ...extension[1] }; updatedExtensions.push(profileExtension); diff --git a/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 2186e8d1d10..a15e92a42ab 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/code/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/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts index a9af5bf9c38..b5fdeb10be1 100644 --- a/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/code/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -34,6 +34,8 @@ import { ExtensionSignatureVerificationCode, computeSize, IAllowedExtensionsService, + VerifyExtensionSignatureConfigKey, + shouldRequireRepositorySignatureFor, } from '../common/extensionManagement.js'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js'; @@ -330,11 +332,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> { if (verifySignature) { - const value = this.configurationService.getValue('extensions.verifySignature'); + const value = this.configurationService.getValue(VerifyExtensionSignatureConfigKey); verifySignature = isBoolean(value) ? value : true; } const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform); - const shouldRequireSignature = (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned; + const shouldRequireSignature = shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest()); if ( verificationStatus !== ExtensionSignatureVerificationCode.Success diff --git a/code/src/vs/platform/extensions/common/extensions.ts b/code/src/vs/platform/extensions/common/extensions.ts index b38ba82a1d9..8e68cbe06d1 100644 --- a/code/src/vs/platform/extensions/common/extensions.ts +++ b/code/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/code/src/vs/platform/extensions/common/extensionsApiProposals.ts b/code/src/vs/platform/extensions/common/extensionsApiProposals.ts index 31fa610d64c..bef924d6d9b 100644 --- a/code/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/code/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -12,6 +12,9 @@ const _allApiProposals = { aiRelatedInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', }, + aiSettingsSearch: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts', + }, aiTextSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', version: 2 @@ -33,7 +36,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 6 + version: 9 }, chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', @@ -154,7 +157,7 @@ const _allApiProposals = { }, defaultChatParticipant: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', - version: 3 + version: 4 }, diffCommand: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', @@ -228,13 +231,11 @@ const _allApiProposals = { }, languageModelDataPart: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts', + version: 2 }, languageModelSystem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', }, - languageModelToolsForAgent: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelToolsForAgent.d.ts', - }, languageStatusText: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', }, @@ -376,9 +377,6 @@ const _allApiProposals = { testRelatedCode: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts', }, - textDocumentEncoding: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts', - }, textEditorDiffInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts', }, @@ -397,6 +395,9 @@ const _allApiProposals = { tokenInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', }, + toolProgress: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.toolProgress.d.ts', + }, treeViewActiveItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', }, diff --git a/code/src/vs/platform/files/common/diskFileSystemProvider.ts b/code/src/vs/platform/files/common/diskFileSystemProvider.ts index ab382fef438..0cbcc333af8 100644 --- a/code/src/vs/platform/files/common/diskFileSystemProvider.ts +++ b/code/src/vs/platform/files/common/diskFileSystemProvider.ts @@ -63,12 +63,26 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen return this.watchNonRecursive(resource, opts); } + private getRefreshWatchersDelay(count: number): number { + if (count > 200) { + // If there are many requests to refresh, start to throttle + // the refresh to reduce pressure. We see potentially thousands + // of requests coming in on startup repeatedly so we take it easy. + return 500; + } + + // By default, use a short delay to keep watchers updating fast but still + // with a delay so that we can efficiently deduplicate requests or reuse + // existing watchers. + return 0; + } + //#region File Watching (universal) private universalWatcher: AbstractUniversalWatcherClient | undefined; private readonly universalWatchRequests: IUniversalWatchRequest[] = []; - private readonly universalWatchRequestDelayer = this._register(new ThrottledDelayer(0)); + private readonly universalWatchRequestDelayer = this._register(new ThrottledDelayer(this.getRefreshWatchersDelay(this.universalWatchRequests.length))); private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable { const request = this.toWatchRequest(resource, opts); @@ -114,12 +128,9 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen } private refreshUniversalWatchers(): void { - - // Buffer requests for universal watching to decide on right watcher - // that supports potentially watching more than one path at once this.universalWatchRequestDelayer.trigger(() => { return this.doRefreshUniversalWatchers(); - }).catch(error => onUnexpectedError(error)); + }, this.getRefreshWatchersDelay(this.universalWatchRequests.length)).catch(error => onUnexpectedError(error)); } private doRefreshUniversalWatchers(): Promise { @@ -155,7 +166,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen private nonRecursiveWatcher: AbstractNonRecursiveWatcherClient | undefined; private readonly nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = []; - private readonly nonRecursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); + private readonly nonRecursiveWatchRequestDelayer = this._register(new ThrottledDelayer(this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length))); private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable { @@ -184,12 +195,9 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen } private refreshNonRecursiveWatchers(): void { - - // Buffer requests for nonrecursive watching to decide on right watcher - // that supports potentially watching more than one path at once this.nonRecursiveWatchRequestDelayer.trigger(() => { return this.doRefreshNonRecursiveWatchers(); - }).catch(error => onUnexpectedError(error)); + }, this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length)).catch(error => onUnexpectedError(error)); } private doRefreshNonRecursiveWatchers(): Promise { diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index 6b40b9d4ba3..47d1f2664f0 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -9,6 +9,8 @@ import { BaseWatcher } from '../baseWatcher.js'; import { isLinux } from '../../../../../base/common/platform.js'; import { INonRecursiveWatchRequest, INonRecursiveWatcher, IRecursiveWatcherWithSubscribe } from '../../../common/watcher.js'; import { NodeJSFileWatcherLibrary } from './nodejsWatcherLib.js'; +import { ThrottledWorker } from '../../../../../base/common/async.js'; +import { MutableDisposable } from '../../../../../base/common/lifecycle.js'; export interface INodeJSWatcherInstance { @@ -30,6 +32,8 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { private readonly _watchers = new Map(); get watchers() { return this._watchers.values(); } + private readonly worker = this._register(new MutableDisposable>()); + constructor(protected readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined) { super(); } @@ -61,15 +65,36 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher { this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`); } + // Stop the worker + this.worker.clear(); + // Stop watching as instructed for (const watcher of watchersToStop) { this.stopWatching(watcher); } // Start watching as instructed - for (const request of requestsToStart) { - this.startWatching(request); - } + this.createWatchWorker().work(requestsToStart); + } + + private createWatchWorker(): ThrottledWorker { + + // We see very large amount of non-recursive file watcher requests + // in large workspaces. To prevent the overhead of starting thousands + // of watchers at once, we use a throttled worker to distribute this + // work over time. + + this.worker.value = new ThrottledWorker({ + maxWorkChunkSize: 100, // only start 100 watchers at once before... + throttleDelay: 100, // ...resting for 100ms until we start watchers again... + maxBufferedWork: Number.MAX_VALUE // ...and never refuse any work. + }, requests => { + for (const request of requests) { + this.startWatching(request); + } + }); + + return this.worker.value; } private requestToWatcherKey(request: INonRecursiveWatchRequest): string | number { diff --git a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 3d92b0e3361..26a54be46ff 100644 --- a/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/code/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -190,6 +190,10 @@ export class NodeJSFileWatcherLibrary extends Disposable { private async doWatchWithNodeJS(isDirectory: boolean, disposables: DisposableStore): Promise { const realPath = await this.realPath.value; + if (this.cts.token.isCancellationRequested) { + return; + } + // macOS: watching samba shares can crash VSCode so we do // a simple check for the file path pointing to /Volumes // (https://github.com/microsoft/vscode/issues/106879) @@ -429,10 +433,12 @@ export class NodeJSFileWatcherLibrary extends Disposable { } }); } catch (error) { - if (!cts.token.isCancellationRequested) { - this.error(`Failed to watch ${realPath} for changes using fs.watch() (${error.toString()})`); + if (cts.token.isCancellationRequested) { + return; } + this.error(`Failed to watch ${realPath} for changes using fs.watch() (${error.toString()})`); + this.notifyWatchFailed(); } } diff --git a/code/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts b/code/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts index f32f480f0b8..5f37ab3c587 100644 --- a/code/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts +++ b/code/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { getCompressedContent, IJSONSchema } from '../../../base/common/jsonSchema.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import * as platform from '../../registry/common/platform.js'; export const Extensions = { @@ -65,15 +65,15 @@ function normalizeId(id: string) { -class JSONContributionRegistry implements IJSONContributionRegistry { +class JSONContributionRegistry extends Disposable implements IJSONContributionRegistry { private readonly schemasById: { [id: string]: IJSONSchema } = {}; private readonly schemaAssociations: { [uri: string]: string[] } = {}; - private readonly _onDidChangeSchema = new Emitter(); + private readonly _onDidChangeSchema = this._register(new Emitter()); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; - private readonly _onDidChangeSchemaAssociations = new Emitter(); + private readonly _onDidChangeSchemaAssociations = this._register(new Emitter()); readonly onDidChangeSchemaAssociations: Event = this._onDidChangeSchemaAssociations.event; public registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema, store?: DisposableStore): void { diff --git a/code/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/code/src/vs/platform/keybinding/common/abstractKeybindingService.ts index f3e1e2b854d..ee26e7b40b9 100644 --- a/code/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/code/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { IME } from '../../../base/common/ime.js'; import { KeyCode } from '../../../base/common/keyCodes.js'; import { Keybinding, ResolvedChord, ResolvedKeybinding, SingleModifierChord } from '../../../base/common/keybindings.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; import * as nls from '../../../nls.js'; import { ICommandService } from '../../commands/common/commands.js'; @@ -20,7 +20,7 @@ import { IKeybindingService, IKeyboardEvent, KeybindingsSchemaContribution } fro import { ResolutionResult, KeybindingResolver, ResultKind, NoMatchingKb } from './keybindingResolver.js'; import { ResolvedKeybindingItem } from './resolvedKeybindingItem.js'; import { ILogService } from '../../log/common/log.js'; -import { INotificationService } from '../../notification/common/notification.js'; +import { INotificationService, IStatusHandle } from '../../notification/common/notification.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; interface CurrentChord { @@ -49,7 +49,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChords: CurrentChord[]; private _currentChordChecker: IntervalTimer; - private _currentChordStatusMessage: IDisposable | null; + private _currentChordStatusMessage: IStatusHandle | null; private _ignoreSingleModifiers: KeybindingModifierSet; private _currentSingleModifier: SingleModifierChord | null; private _currentSingleModifierClearTimeout: TimeoutTimer; @@ -203,7 +203,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _leaveChordMode(): void { if (this._currentChordStatusMessage) { - this._currentChordStatusMessage.dispose(); + this._currentChordStatusMessage.close(); this._currentChordStatusMessage = null; } this._currentChordChecker.cancel(); diff --git a/code/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/code/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 4a5f4c83407..04af18c812b 100644 --- a/code/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/code/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -184,7 +184,7 @@ suite('AbstractKeybindingService', () => { status(message: string, options?: IStatusMessageOptions) { statusMessageCalls!.push(message); return { - dispose: () => { + close: () => { statusMessageCallsDisposed!.push(message); } }; diff --git a/code/src/vs/platform/markers/common/markerService.ts b/code/src/vs/platform/markers/common/markerService.ts index 68e6d0391bd..7e5b2d10cd8 100644 --- a/code/src/vs/platform/markers/common/markerService.ts +++ b/code/src/vs/platform/markers/common/markerService.ts @@ -11,7 +11,7 @@ import { ResourceMap, ResourceSet } from '../../../base/common/map.js'; import { Schemas } from '../../../base/common/network.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; -import { IMarker, IMarkerData, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js'; +import { IMarker, IMarkerData, IMarkerReadOptions, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js'; export const unsupportedSchemas = new Set([ Schemas.inMemory, @@ -326,7 +326,7 @@ export class MarkerService implements IMarkerService { }; } - read(filter: { owner?: string; resource?: URI; severities?: number; take?: number } = Object.create(null)): IMarker[] { + read(filter: IMarkerReadOptions = Object.create(null)): IMarker[] { let { owner, resource, severities, take } = filter; @@ -336,7 +336,7 @@ export class MarkerService implements IMarkerService { if (owner && resource) { // exactly one owner AND resource - const reasons = this._filteredResources.get(resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined; if (reasons?.length) { const infoMarker = this._createFilteredMarker(resource, reasons); return [infoMarker]; @@ -352,7 +352,7 @@ export class MarkerService implements IMarkerService { if (take > 0 && result.length === take) { break; } - const reasons = this._filteredResources.get(resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined; if (reasons?.length) { result.push(this._createFilteredMarker(resource, reasons)); @@ -379,7 +379,7 @@ export class MarkerService implements IMarkerService { if (take > 0 && result.length === take) { break; } - const reasons = this._filteredResources.get(data.resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(data.resource) : undefined; if (reasons?.length) { result.push(this._createFilteredMarker(data.resource, reasons)); filtered.add(data.resource); diff --git a/code/src/vs/platform/markers/common/markers.ts b/code/src/vs/platform/markers/common/markers.ts index 2c7f1668514..cc686747a83 100644 --- a/code/src/vs/platform/markers/common/markers.ts +++ b/code/src/vs/platform/markers/common/markers.ts @@ -10,6 +10,14 @@ import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; +export interface IMarkerReadOptions { + owner?: string; + resource?: URI; + severities?: number; + take?: number; + ignoreResourceFilters?: boolean; +} + export interface IMarkerService { readonly _serviceBrand: undefined; @@ -21,7 +29,7 @@ export interface IMarkerService { remove(owner: string, resources: URI[]): void; - read(filter?: { owner?: string; resource?: URI; severities?: number; take?: number }): IMarker[]; + read(filter?: IMarkerReadOptions): IMarker[]; installResourceFilter(resource: URI, reason: string): IDisposable; diff --git a/code/src/vs/platform/markers/test/common/markerService.test.ts b/code/src/vs/platform/markers/test/common/markerService.test.ts index 4ccdd62b50e..adcf3760025 100644 --- a/code/src/vs/platform/markers/test/common/markerService.test.ts +++ b/code/src/vs/platform/markers/test/common/markerService.test.ts @@ -243,6 +243,39 @@ suite('Marker Service', () => { assert.strictEqual(service.read({ resource: resource2 }).length, 1); }); + test('resource filter hides markers for the filtered resource UNLESS explicit read', () => { + service = new markerService.MarkerService(); + const resource1 = URI.parse('file:///path/file1.cs'); + const resource2 = URI.parse('file:///path/file2.cs'); + + // Add markers to both resources + service.changeOne('owner1', resource1, [randomMarkerData()]); + service.changeOne('owner1', resource2, [randomMarkerData()]); + + // Verify both resources have markers + assert.strictEqual(service.read().length, 2); + assert.strictEqual(service.read({ resource: resource1 }).length, 1); + assert.strictEqual(service.read({ resource: resource2 }).length, 1); + + // Install filter for resource1 + const filter = service.installResourceFilter(resource1, 'Test filter'); + + // Verify resource1 markers are filtered out, but have 1 info marker instead + assert.strictEqual(service.read().length, 2); // 1 real + 1 info + assert.strictEqual(service.read({ resource: resource1 }).length, 1); // 1 info + assert.strictEqual(service.read({ resource: resource2 }).length, 1); + + // Verify resource1 markers are visible again + assert.strictEqual(service.read({ ignoreResourceFilters: true }).length, 2); + assert.strictEqual(service.read({ resource: resource1, ignoreResourceFilters: true }).length, 1); + assert.strictEqual(service.read({ resource: resource1, ignoreResourceFilters: true })[0].severity, MarkerSeverity.Error); + assert.strictEqual(service.read({ resource: resource2, ignoreResourceFilters: true }).length, 1); + assert.strictEqual(service.read({ resource: resource2, ignoreResourceFilters: true })[0].severity, MarkerSeverity.Error); + + // Dispose filter + filter.dispose(); + }); + test('resource filter affects all filter combinations', () => { service = new markerService.MarkerService(); const resource = URI.parse('file:///path/file.cs'); diff --git a/code/src/vs/platform/mcp/common/mcpManagementCli.ts b/code/src/vs/platform/mcp/common/mcpManagementCli.ts index 55106298512..cecdfaa07a4 100644 --- a/code/src/vs/platform/mcp/common/mcpManagementCli.ts +++ b/code/src/vs/platform/mcp/common/mcpManagementCli.ts @@ -5,9 +5,9 @@ import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ILogger } from '../../log/common/log.js'; -import { IMcpConfiguration, IMcpConfigurationSSE, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js'; +import { IMcpConfiguration, IMcpConfigurationHTTP, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js'; -type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationSSE }; +type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationHTTP }; export class McpManagementCli { constructor( @@ -51,7 +51,7 @@ export class McpManagementCli { } const { name, ...rest } = parsed; - return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationSSE }; + return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationHTTP }; } } diff --git a/code/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/code/src/vs/platform/mcp/common/mcpPlatformTypes.ts index b16d59e2934..078c70306cf 100644 --- a/code/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/code/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -7,10 +7,10 @@ export interface IMcpConfiguration { inputs?: unknown[]; /** @deprecated Only for rough cross-compat with other formats */ mcpServers?: Record; - servers?: Record; + servers?: Record; } -export type McpConfigurationServer = IMcpConfigurationStdio | IMcpConfigurationSSE; +export type McpConfigurationServer = IMcpConfigurationStdio | IMcpConfigurationHTTP; export interface IMcpConfigurationStdio { type?: 'stdio'; @@ -20,8 +20,8 @@ export interface IMcpConfigurationStdio { envFile?: string; } -export interface IMcpConfigurationSSE { - type: 'sse'; +export interface IMcpConfigurationHTTP { + type?: 'http'; url: string; headers?: Record; } diff --git a/code/src/vs/platform/native/common/native.ts b/code/src/vs/platform/native/common/native.ts index 4d60c1d9f1b..9394408888f 100644 --- a/code/src/vs/platform/native/common/native.ts +++ b/code/src/vs/platform/native/common/native.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from '../../../base/common/buffer.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; import { Event } from '../../../base/common/event.js'; import { URI } from '../../../base/common/uri.js'; import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from '../../../base/parts/sandbox/common/electronTypes.js'; @@ -38,6 +39,12 @@ export interface INativeHostOptions { readonly targetWindowId?: number; } +export interface IElementData { + readonly outerHTML: string; + readonly computedStyle: string; + readonly bounds: IRectangle; +} + export interface ICommonNativeHostService { readonly _serviceBrand: undefined; @@ -55,6 +62,7 @@ export interface ICommonNativeHostService { readonly onDidBlurMainWindow: Event; readonly onDidChangeWindowFullScreen: Event<{ windowId: number; fullscreen: boolean }>; + readonly onDidChangeWindowAlwaysOnTop: Event<{ windowId: number; alwaysOnTop: boolean }>; readonly onDidFocusMainOrAuxiliaryWindow: Event; readonly onDidBlurMainOrAuxiliaryWindow: Event; @@ -92,6 +100,10 @@ export interface ICommonNativeHostService { moveWindowTop(options?: INativeHostOptions): Promise; positionWindow(position: IRectangle, options?: INativeHostOptions): Promise; + isWindowAlwaysOnTop(options?: INativeHostOptions): Promise; + toggleWindowAlwaysOnTop(options?: INativeHostOptions): Promise; + setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise; + /** * Only supported on Windows and macOS. Updates the window controls to match the title bar size. * @@ -143,13 +155,15 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Screenshots - getScreenshot(): Promise; + getScreenshot(rect?: IRectangle): Promise; + getElementData(rect: IRectangle, token: CancellationToken, cancellationId?: number): Promise; // Process getProcessId(): Promise; killProcess(pid: number, code: string): Promise; // Clipboard + triggerPaste(options?: INativeHostOptions): Promise; readClipboardText(type?: 'selection' | 'clipboard'): Promise; writeClipboardText(text: string, type?: 'selection' | 'clipboard'): Promise; readClipboardFindText(): Promise; diff --git a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts index 6324e98e290..45964bcdcc2 100644 --- a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -28,7 +28,7 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; -import { ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js'; +import { ICommonNativeHostService, IElementData, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js'; import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; @@ -48,11 +48,18 @@ import { IConfigurationService } from '../../configuration/common/configuration. import { IProxyAuthService } from './auth.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; import { randomPath } from '../../../base/common/extpath.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } export const INativeHostMainService = createDecorator('nativeHostMainService'); +interface NodeDataResponse { + outerHTML: string; + computedStyle: string; + bounds: IRectangle; +} + export class NativeHostMainService extends Disposable implements INativeHostMainService { declare readonly _serviceBrand: undefined; @@ -97,6 +104,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain Event.map(this.auxiliaryWindowsMainService.onDidChangeFullScreen, e => ({ windowId: e.window.id, fullscreen: e.fullscreen })) ); + this.onDidChangeWindowAlwaysOnTop = Event.any( + Event.None, // always on top is unsupported in main windows currently + Event.map(this.auxiliaryWindowsMainService.onDidChangeAlwaysOnTop, e => ({ windowId: e.window.id, alwaysOnTop: e.alwaysOnTop })) + ); + this.onDidBlurMainWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)); this.onDidFocusMainWindow = Event.any( Event.map(Event.filter(Event.map(this.windowsMainService.onDidChangeWindowsCount, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id), @@ -154,6 +166,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain readonly onDidBlurMainOrAuxiliaryWindow: Event; readonly onDidFocusMainOrAuxiliaryWindow: Event; + readonly onDidChangeWindowAlwaysOnTop: Event<{ readonly windowId: number; readonly alwaysOnTop: boolean }>; + readonly onDidResumeOS: Event; readonly onDidChangeColorScheme: Event; @@ -304,6 +318,21 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.win?.moveTop(); } + async isWindowAlwaysOnTop(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + return window?.win?.isAlwaysOnTop() ?? false; + } + + async toggleWindowAlwaysOnTop(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + window?.win?.setAlwaysOnTop(!window.win.isAlwaysOnTop()); + } + + async setWindowAlwaysOnTop(windowId: number | undefined, alwaysOnTop: boolean, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + window?.win?.setAlwaysOnTop(alwaysOnTop); + } + async positionWindow(windowId: number | undefined, position: IRectangle, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); if (window?.win) { @@ -720,11 +749,318 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Screenshots - async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise { + async getScreenshot(windowId: number | undefined, rect?: IRectangle, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); - const captured = await window?.win?.webContents.capturePage(); + const captured = await window?.win?.webContents.capturePage(rect); - return captured?.toJPEG(95); + const buf = captured?.toJPEG(95); + return buf && VSBuffer.wrap(buf); + } + + async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, cancellationId?: number): Promise { + const window = this.windowById(windowId, windowId); + if (!window?.win) { + return undefined; + } + + // Find the simple browser webview + const allWebContents = webContents.getAllWebContents(); + const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === windowId); + + if (!simpleBrowserWebview) { + return undefined; + } + + const debuggers = simpleBrowserWebview.debugger; + debuggers.attach(); + + const { targetInfos } = await debuggers.sendCommand('Target.getTargets'); + let resultId: string | undefined = undefined; + let target: typeof targetInfos[number] | undefined = undefined; + let targetSessionId: number | undefined = undefined; + try { + // find parent id and extract id + const matchingTarget = targetInfos.find((targetInfo: { url: string }) => { + const url = new URL(targetInfo.url); + return url.searchParams.get('parentId') === window?.id.toString() && url.searchParams.get('extensionId') === 'vscode.simple-browser'; + }); + + if (matchingTarget) { + const url = new URL(matchingTarget.url); + resultId = url.searchParams.get('id')!; + } + + // use id to grab simple browser target + if (resultId) { + target = targetInfos.find((targetInfo: { url: string }) => { + const url = new URL(targetInfo.url); + return url.searchParams.get('id') === resultId && url.searchParams.get('vscodeBrowserReqId')!; + }); + } + + const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', { + targetId: target.targetId, + flatten: true, + }); + + targetSessionId = sessionId; + + await debuggers.sendCommand('DOM.enable', {}, sessionId); + await debuggers.sendCommand('CSS.enable', {}, sessionId); + await debuggers.sendCommand('Overlay.enable', {}, sessionId); + await debuggers.sendCommand('Debugger.enable', {}, sessionId); + await debuggers.sendCommand('Runtime.enable', {}, sessionId); + + await debuggers.sendCommand('Runtime.evaluate', { + expression: `(function() { + const style = document.createElement('style'); + style.id = '__pseudoBlocker__'; + style.textContent = '*::before, *::after { pointer-events: none !important; }'; + document.head.appendChild(style); + })();`, + }, sessionId); + + // slightly changed default CDP debugger inspect colors + await debuggers.sendCommand('Overlay.setInspectMode', { + mode: 'searchForNode', + highlightConfig: { + showInfo: true, + showRulers: false, + showStyles: true, + showAccessibilityInfo: true, + showExtensionLines: false, + contrastAlgorithm: 'aa', + contentColor: { r: 173, g: 216, b: 255, a: 0.8 }, + paddingColor: { r: 150, g: 200, b: 255, a: 0.5 }, + borderColor: { r: 120, g: 180, b: 255, a: 0.7 }, + marginColor: { r: 200, g: 220, b: 255, a: 0.4 }, + eventTargetColor: { r: 130, g: 160, b: 255, a: 0.8 }, + shapeColor: { r: 130, g: 160, b: 255, a: 0.8 }, + shapeMarginColor: { r: 130, g: 160, b: 255, a: 0.5 }, + gridHighlightConfig: { + rowGapColor: { r: 140, g: 190, b: 255, a: 0.3 }, + rowHatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + columnGapColor: { r: 140, g: 190, b: 255, a: 0.3 }, + columnHatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + rowLineColor: { r: 120, g: 180, b: 255 }, + columnLineColor: { r: 120, g: 180, b: 255 }, + rowLineDash: true, + columnLineDash: true + }, + flexContainerHighlightConfig: { + containerBorder: { + color: { r: 120, g: 180, b: 255 }, + pattern: 'solid' + }, + itemSeparator: { + color: { r: 140, g: 190, b: 255 }, + pattern: 'solid' + }, + lineSeparator: { + color: { r: 140, g: 190, b: 255 }, + pattern: 'solid' + }, + mainDistributedSpace: { + hatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + fillColor: { r: 140, g: 190, b: 255, a: 0.4 } + }, + crossDistributedSpace: { + hatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + fillColor: { r: 140, g: 190, b: 255, a: 0.4 } + }, + rowGapSpace: { + hatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + fillColor: { r: 140, g: 190, b: 255, a: 0.4 } + }, + columnGapSpace: { + hatchColor: { r: 140, g: 190, b: 255, a: 0.7 }, + fillColor: { r: 140, g: 190, b: 255, a: 0.4 } + } + }, + flexItemHighlightConfig: { + baseSizeBox: { + hatchColor: { r: 130, g: 170, b: 255, a: 0.6 } + }, + baseSizeBorder: { + color: { r: 120, g: 180, b: 255 }, + pattern: 'solid' + }, + flexibilityArrow: { + color: { r: 130, g: 190, b: 255 } + } + }, + }, + }, sessionId); + } catch (e) { + debuggers.detach(); + throw new Error('No target found', e); + } + + if (!targetSessionId) { + debuggers.detach(); + throw new Error('No target session id found'); + } + + const nodeData = await this.getNodeData(targetSessionId, debuggers, window.win, cancellationId); + debuggers.detach(); + + const zoomFactor = simpleBrowserWebview.getZoomFactor(); + const absoluteBounds = { + x: rect.x + nodeData.bounds.x, + y: rect.y + nodeData.bounds.y, + width: nodeData.bounds.width, + height: nodeData.bounds.height + }; + + const clippedBounds = { + x: Math.max(absoluteBounds.x, rect.x), + y: Math.max(absoluteBounds.y, rect.y), + width: Math.max(0, Math.min(absoluteBounds.x + absoluteBounds.width, rect.x + rect.width) - Math.max(absoluteBounds.x, rect.x)), + height: Math.max(0, Math.min(absoluteBounds.y + absoluteBounds.height, rect.y + rect.height) - Math.max(absoluteBounds.y, rect.y)) + }; + + const scaledBounds = { + x: clippedBounds.x * zoomFactor, + y: clippedBounds.y * zoomFactor, + width: clippedBounds.width * zoomFactor, + height: clippedBounds.height * zoomFactor + }; + + return { outerHTML: nodeData.outerHTML, computedStyle: nodeData.computedStyle, bounds: scaledBounds }; + } + + async getNodeData(sessionId: number, debuggers: any, window: BrowserWindow, cancellationId?: number): Promise { + return new Promise((resolve, reject) => { + const onMessage = async (event: any, method: string, params: { backendNodeId: number }) => { + if (method === 'Overlay.inspectNodeRequested') { + debuggers.off('message', onMessage); + await debuggers.sendCommand('Runtime.evaluate', { + expression: `(() => { + const style = document.getElementById('__pseudoBlocker__'); + if (style) style.remove(); + })();`, + }, sessionId); + + const backendNodeId = params?.backendNodeId; + if (!backendNodeId) { + throw new Error('Missing backendNodeId in inspectNodeRequested event'); + } + + try { + await debuggers.sendCommand('DOM.getDocument', {}, sessionId); + const { nodeIds } = await debuggers.sendCommand('DOM.pushNodesByBackendIdsToFrontend', { backendNodeIds: [backendNodeId] }, sessionId); + if (!nodeIds || nodeIds.length === 0) { + throw new Error('Failed to get node IDs.'); + } + const nodeId = nodeIds[0]; + + const { model } = await debuggers.sendCommand('DOM.getBoxModel', { nodeId }, sessionId); + if (!model) { + throw new Error('Failed to get box model.'); + } + + const content = model.content; + const margin = model.margin; + const x = Math.min(margin[0], content[0]); + const y = Math.min(margin[1], content[1]) + 32.4; // 32.4 is height of the title bar + const width = Math.max(margin[2] - margin[0], content[2] - content[0]); + const height = Math.max(margin[5] - margin[1], content[5] - content[1]); + + const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId); + if (!matched) { + throw new Error('Failed to get matched css.'); + } + + const formatted = this.formatMatchedStyles(matched); + const { outerHTML } = await debuggers.sendCommand('DOM.getOuterHTML', { nodeId }, sessionId); + if (!outerHTML) { + throw new Error('Failed to get outerHTML.'); + } + + resolve({ + outerHTML, + computedStyle: formatted, + bounds: { x, y, width, height } + }); + } catch (err) { + debuggers.off('message', onMessage); + debuggers.detach(); + reject(err); + + } + } + }; + + window.webContents.on('ipc-message', async (event, channel, closedCancellationId) => { + if (channel === `vscode:cancelElementSelection${cancellationId}`) { + if (cancellationId !== closedCancellationId) { + return; + } + debuggers.off('message', onMessage); + if (debuggers.isAttached()) { + debuggers.detach(); + } + window.webContents.removeAllListeners('ipc-message'); + } + }); + + debuggers.on('message', onMessage); + }); + } + + formatMatchedStyles(matched: any): string { + const lines: string[] = []; + + // inline + if (matched.inlineStyle?.cssProperties?.length) { + lines.push('/* Inline style */'); + lines.push('element {'); + for (const prop of matched.inlineStyle.cssProperties) { + if (prop.name && prop.value) { + lines.push(` ${prop.name}: ${prop.value};`); + } + } + lines.push('}\n'); + } + + // matched + if (matched.matchedCSSRules?.length) { + for (const ruleEntry of matched.matchedCSSRules) { + const rule = ruleEntry.rule; + const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', '); + lines.push(`/* Matched Rule from ${rule.origin} */`); + lines.push(`${selectors} {`); + for (const prop of rule.style.cssProperties) { + if (prop.name && prop.value) { + lines.push(` ${prop.name}: ${prop.value};`); + } + } + lines.push('}\n'); + } + } + + // inherited rules + if (matched.inherited?.length) { + let level = 1; + for (const inherited of matched.inherited) { + const rules = inherited.matchedCSSRules || []; + for (const ruleEntry of rules) { + const rule = ruleEntry.rule; + const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', '); + lines.push(`/* Inherited from ancestor level ${level} (${rule.origin}) */`); + lines.push(`${selectors} {`); + for (const prop of rule.style.cssProperties) { + if (prop.name && prop.value) { + lines.push(` ${prop.name}: ${prop.value};`); + } + } + lines.push('}\n'); + } + level++; + } + } + + return '\n' + lines.join('\n'); } //#endregion @@ -750,6 +1086,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return clipboard.readText(type); } + async triggerPaste(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + return window?.win?.webContents.paste() ?? Promise.resolve(); + } + async readImage(): Promise { return clipboard.readImage().toPNG(); } diff --git a/code/src/vs/platform/notification/common/notification.ts b/code/src/vs/platform/notification/common/notification.ts index f573e788fb3..a3b59ab038e 100644 --- a/code/src/vs/platform/notification/common/notification.ts +++ b/code/src/vs/platform/notification/common/notification.ts @@ -5,7 +5,6 @@ import { IAction } from '../../../base/common/actions.js'; import { Event } from '../../../base/common/event.js'; -import { IDisposable } from '../../../base/common/lifecycle.js'; import BaseSeverity from '../../../base/common/severity.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; @@ -22,6 +21,11 @@ export enum NotificationPriority { */ DEFAULT, + /** + * Optional priority: notification might only be visible from the notifications center. + */ + OPTIONAL, + /** * Silent priority: notification will only be visible from the notifications center. */ @@ -268,6 +272,14 @@ export interface INotificationHandle { close(): void; } +export interface IStatusHandle { + + /** + * Hide the status message. + */ + close(): void; +} + interface IBasePromptChoice { /** @@ -445,9 +457,9 @@ export interface INotificationService { * @param message the message to show as status * @param options provides some optional configuration options * - * @returns a disposable to hide the status message + * @returns a handle to hide the status message */ - status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle; } export class NoOpNotification implements INotificationHandle { diff --git a/code/src/vs/platform/notification/test/common/testNotificationService.ts b/code/src/vs/platform/notification/test/common/testNotificationService.ts index f016e186cf5..80def17f244 100644 --- a/code/src/vs/platform/notification/test/common/testNotificationService.ts +++ b/code/src/vs/platform/notification/test/common/testNotificationService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { INotification, INotificationHandle, INotificationService, INotificationSource, INotificationSourceFilter, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NotificationsFilter, Severity } from '../../common/notification.js'; +import { INotification, INotificationHandle, INotificationService, INotificationSource, INotificationSourceFilter, IPromptChoice, IPromptOptions, IStatusHandle, IStatusMessageOptions, NoOpNotification, NotificationsFilter, Severity } from '../../common/notification.js'; export class TestNotificationService implements INotificationService { @@ -39,8 +38,10 @@ export class TestNotificationService implements INotificationService { return TestNotificationService.NO_OP; } - status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { + close: () => { } + }; } setFilter(): void { } diff --git a/code/src/vs/platform/observable/common/observableMemento.ts b/code/src/vs/platform/observable/common/observableMemento.ts index cf38e2fb80b..f901636f2b2 100644 --- a/code/src/vs/platform/observable/common/observableMemento.ts +++ b/code/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/code/src/vs/platform/process/electron-main/processMainService.ts b/code/src/vs/platform/process/electron-main/processMainService.ts index d2f6bbfd98a..df2af9b7a85 100644 --- a/code/src/vs/platform/process/electron-main/processMainService.ts +++ b/code/src/vs/platform/process/electron-main/processMainService.ts @@ -33,7 +33,6 @@ interface IBrowserWindowOptions { backgroundColor: string | undefined; title: string; zoomLevel: number; - alwaysOnTop: boolean; } type IStrictWindowState = Required>; @@ -145,8 +144,7 @@ export class ProcessMainService implements IProcessMainService { this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, { backgroundColor: data.styles.backgroundColor, title: localize('processExplorer', "Process Explorer"), - zoomLevel: data.zoomLevel, - alwaysOnTop: true + zoomLevel: data.zoomLevel }, 'process-explorer'); // Store into config object URL @@ -349,7 +347,7 @@ export class ProcessMainService implements IProcessMainService { zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), sandbox: true }, - alwaysOnTop: options.alwaysOnTop, + alwaysOnTop: true, experimentalDarkMode: true }; const window = new BrowserWindow(browserWindowOptions); diff --git a/code/src/vs/platform/prompts/common/config.ts b/code/src/vs/platform/prompts/common/config.ts index 01f4e265cae..1b992112e08 100644 --- a/code/src/vs/platform/prompts/common/config.ts +++ b/code/src/vs/platform/prompts/common/config.ts @@ -5,11 +5,11 @@ import { ContextKeyExpr } from '../../contextkey/common/contextkey.js'; import type { IConfigurationService } from '../../configuration/common/configuration.js'; -import { CONFIG_KEY, DEFAULT_SOURCE_FOLDER, LOCATIONS_CONFIG_KEY } from './constants.js'; +import { CONFIG_KEY, PROMPT_DEFAULT_SOURCE_FOLDER, INSTRUCTIONS_LOCATIONS_CONFIG_KEY, PROMPT_LOCATIONS_CONFIG_KEY, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER } from './constants.js'; /** * Configuration helper for the `reusable prompts` feature. - * @see {@link CONFIG_KEY} and {@link LOCATIONS_CONFIG_KEY}. + * @see {@link CONFIG_KEY}, {@link PROMPT_LOCATIONS_CONFIG_KEY} and {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}. * * ### Functions * @@ -29,7 +29,8 @@ import { CONFIG_KEY, DEFAULT_SOURCE_FOLDER, LOCATIONS_CONFIG_KEY } from './const */ export namespace PromptsConfig { export const KEY = CONFIG_KEY; - export const LOCATIONS_KEY = LOCATIONS_CONFIG_KEY; + export const PROMPT_LOCATIONS_KEY = PROMPT_LOCATIONS_CONFIG_KEY; + export const INSTRUCTIONS_LOCATION_KEY = INSTRUCTIONS_LOCATIONS_CONFIG_KEY; /** * Checks if the feature is enabled. @@ -50,12 +51,14 @@ export namespace PromptsConfig { /** * Get value of the `reusable prompt locations` configuration setting. - * @see {@link LOCATIONS_CONFIG_KEY}. + * @see {@link PROMPT_LOCATIONS_CONFIG_KEY} or {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}. */ export const getLocationsValue = ( configService: IConfigurationService, + type: 'instructions' | 'prompt' ): Record | undefined => { - const configValue = configService.getValue(LOCATIONS_CONFIG_KEY); + const key = type === 'instructions' ? INSTRUCTIONS_LOCATIONS_CONFIG_KEY : PROMPT_LOCATIONS_CONFIG_KEY; + const configValue = configService.getValue(key); if (configValue === undefined || configValue === null || Array.isArray(configValue)) { return undefined; @@ -85,26 +88,28 @@ export namespace PromptsConfig { /** * Gets list of source folders for prompt files. - * Defaults to {@link DEFAULT_SOURCE_FOLDER}. + * Defaults to {@link PROMPT_DEFAULT_SOURCE_FOLDER} or {@link INSTRUCTIONS_DEFAULT_SOURCE_FOLDER}. */ export const promptSourceFolders = ( configService: IConfigurationService, + type: 'instructions' | 'prompt' ): string[] => { - const value = getLocationsValue(configService); + const value = getLocationsValue(configService, type); + const defaultSourceFolder = type === 'instructions' ? INSTRUCTIONS_DEFAULT_SOURCE_FOLDER : PROMPT_DEFAULT_SOURCE_FOLDER; // note! the `value &&` part handles the `undefined`, `null`, and `false` cases if (value && (typeof value === 'object')) { const paths: string[] = []; // if the default source folder is not explicitly disabled, add it - if (value[DEFAULT_SOURCE_FOLDER] !== false) { - paths.push(DEFAULT_SOURCE_FOLDER); + if (value[defaultSourceFolder] !== false) { + paths.push(defaultSourceFolder); } // copy all the enabled paths to the result list for (const [path, enabled] of Object.entries(value)) { // we already added the default source folder, so skip it - if ((enabled === false) || (path === DEFAULT_SOURCE_FOLDER)) { + if ((enabled === false) || (path === defaultSourceFolder)) { continue; } @@ -127,7 +132,7 @@ export namespace PromptsConfig { * be clearly mapped to a boolean (e.g., `"true"`, `"TRUE"`, `"FaLSe"`, etc.), * `undefined` for rest of the values */ -function asBoolean(value: any): boolean | undefined { +export const asBoolean = (value: any): boolean | undefined => { if (typeof value === 'boolean') { return value; } @@ -146,4 +151,4 @@ function asBoolean(value: any): boolean | undefined { } return undefined; -} +}; diff --git a/code/src/vs/platform/prompts/common/constants.ts b/code/src/vs/platform/prompts/common/constants.ts index 99a024e43a5..b4102539986 100644 --- a/code/src/vs/platform/prompts/common/constants.ts +++ b/code/src/vs/platform/prompts/common/constants.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../base/common/uri.js'; -import { assert } from '../../../base/common/assert.js'; import { basename } from '../../../base/common/path.js'; /** @@ -12,6 +11,11 @@ import { basename } from '../../../base/common/path.js'; */ export const PROMPT_FILE_EXTENSION = '.prompt.md'; +/** + * File extension for the reusable instruction files. + */ +export const INSTRUCTION_FILE_EXTENSION = '.instructions.md'; + /** * Copilot custom instructions file name. */ @@ -26,46 +30,84 @@ export const CONFIG_KEY: string = 'chat.promptFiles'; /** * Configuration key for the locations of reusable prompt files. */ -export const LOCATIONS_CONFIG_KEY: string = 'chat.promptFilesLocations'; +export const PROMPT_LOCATIONS_CONFIG_KEY: string = 'chat.promptFilesLocations'; + +/** + * Configuration key for the locations of instructions files. + */ +export const INSTRUCTIONS_LOCATIONS_CONFIG_KEY: string = 'chat.instructionsFilesLocations'; + +/** + * Default reusable prompt files source folder. + */ +export const PROMPT_DEFAULT_SOURCE_FOLDER = '.github/prompts'; /** * Default reusable prompt files source folder. */ -export const DEFAULT_SOURCE_FOLDER = '.github/prompts'; +export const INSTRUCTIONS_DEFAULT_SOURCE_FOLDER = '.github/instructions'; /** - * Check if provided path is a reusable prompt file. + * Gets the prompt file type from the provided path. */ -export const isPromptFile = ( - fileUri: URI, -): boolean => { +export function getPromptFileType(fileUri: URI): 'instructions' | 'prompt' | undefined { const filename = basename(fileUri.path); - const hasPromptFileExtension = filename.endsWith(PROMPT_FILE_EXTENSION); - const isCustomInstructionsFile = (filename === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); + if (filename.endsWith(PROMPT_FILE_EXTENSION)) { + return 'prompt'; + } + + if (filename.endsWith(INSTRUCTION_FILE_EXTENSION) || (filename === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME)) { + return 'instructions'; + } + + return undefined; +} - return hasPromptFileExtension || isCustomInstructionsFile; +/** + * Check if provided URI points to a file that with prompt file extension. + */ +export function isPromptOrInstructionsFile(fileUri: URI): boolean { + return getPromptFileType(fileUri) !== undefined; +} + + +export function getPromptFileExtension(type: 'instructions' | 'prompt'): string { + return type === 'instructions' ? INSTRUCTION_FILE_EXTENSION : PROMPT_FILE_EXTENSION; +} + +/** + * Check whether provided URI belongs to an `untitled` document. + */ +export const isUntitled = ( + fileUri: URI, +): boolean => { + return fileUri.scheme === 'untitled'; }; /** * Gets clean prompt name without file extension. - * - * @throws If provided path is not a prompt file - * (does not end with {@link PROMPT_FILE_EXTENSION}). */ export const getCleanPromptName = ( fileUri: URI, ): string => { - assert( - isPromptFile(fileUri), - `Provided path '${fileUri.fsPath}' is not a prompt file.`, - ); - - // if a Copilot custom instructions file, remove `markdown` file extension - // otherwise, remove the `prompt` file extension - const fileExtension = (fileUri.path.endsWith(COPILOT_CUSTOM_INSTRUCTIONS_FILENAME)) - ? '.md' - : PROMPT_FILE_EXTENSION; - - return basename(fileUri.path, fileExtension); + const fileName = basename(fileUri.path); + + if (fileName.endsWith(PROMPT_FILE_EXTENSION)) { + return basename(fileUri.path, PROMPT_FILE_EXTENSION); + } + + if (fileName.endsWith(INSTRUCTION_FILE_EXTENSION)) { + return basename(fileUri.path, INSTRUCTION_FILE_EXTENSION); + } + + if (fileName === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME) { + return basename(fileUri.path, '.md'); + } + + // because we now rely on the `prompt` language ID that can be explicitly + // set for any document in the editor, any file can be a "prompt" file, so + // to account for that, we return the full file name including the file + // extension for all other cases + return basename(fileUri.path); }; diff --git a/code/src/vs/platform/prompts/test/common/config.test.ts b/code/src/vs/platform/prompts/test/common/config.test.ts index 560a676977a..a895d9b4849 100644 --- a/code/src/vs/platform/prompts/test/common/config.test.ts +++ b/code/src/vs/platform/prompts/test/common/config.test.ts @@ -22,7 +22,7 @@ const createMock = (value: T): IConfigurationService => { ); assert( - [PromptsConfig.KEY, PromptsConfig.LOCATIONS_KEY].includes(key), + [PromptsConfig.KEY, PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY].includes(key), `Unsupported configuration key '${key}'.`, ); @@ -34,12 +34,137 @@ const createMock = (value: T): IConfigurationService => { suite('PromptsConfig', () => { ensureNoDisposablesAreLeakedInTestSuite(); + suite('• enabled', () => { + test('• true', () => { + const configService = createMock(true); + + assert.strictEqual( + PromptsConfig.enabled(configService), + true, + 'Must read correct enablement value.', + ); + }); + + test('• false', () => { + const configService = createMock(false); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• null', () => { + const configService = createMock(null); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• string', () => { + const configService = createMock(''); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• true string', () => { + const configService = createMock('TRUE'); + + assert.strictEqual( + PromptsConfig.enabled(configService), + true, + 'Must read correct enablement value.', + ); + }); + + test('• false string', () => { + const configService = createMock('FaLsE'); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• number', () => { + const configService = createMock(randomInt(100)); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• NaN', () => { + const configService = createMock(NaN); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• bigint', () => { + const configService = createMock(BigInt(randomInt(100))); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• symbol', () => { + const configService = createMock(Symbol('test')); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• object', () => { + const configService = createMock({ + '.github/prompts': false, + }); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• array', () => { + const configService = createMock(['.github/prompts']); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + }); + + suite('• getLocationsValue', () => { test('• undefined', () => { const configService = createMock(undefined); assert.strictEqual( - PromptsConfig.getLocationsValue(configService), + PromptsConfig.getLocationsValue(configService, 'prompt'), undefined, 'Must read correct value.', ); @@ -49,7 +174,7 @@ suite('PromptsConfig', () => { const configService = createMock(null); assert.strictEqual( - PromptsConfig.getLocationsValue(configService), + PromptsConfig.getLocationsValue(configService, 'prompt'), undefined, 'Must read correct value.', ); @@ -58,7 +183,7 @@ suite('PromptsConfig', () => { suite('• object', () => { test('• empty', () => { assert.deepStrictEqual( - PromptsConfig.getLocationsValue(createMock({})), + PromptsConfig.getLocationsValue(createMock({}), 'prompt'), {}, 'Must read correct value.', ); @@ -79,7 +204,7 @@ suite('PromptsConfig', () => { 'some/folder.with.dots/another.file': true, '/var/logs/app.01.05.error': true, './.tempfile': true, - })), + }), 'prompt'), { '/root/.bashrc': true, '../../folder/.hidden-folder/config.xml': true, @@ -123,7 +248,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), { '../assets/img/logo.v2.png': true, '/mnt/storage/video.archive/episode.01.mkv': false, @@ -150,7 +275,7 @@ suite('PromptsConfig', () => { '/var/data/datafile.2025-02-05.json': '\n', '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), { '/mnt/storage/video.archive/episode.01.mkv': false, }, @@ -165,7 +290,7 @@ suite('PromptsConfig', () => { const configService = createMock(undefined); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService, 'prompt'), [], 'Must read correct value.', ); @@ -175,7 +300,7 @@ suite('PromptsConfig', () => { const configService = createMock(null); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService), + PromptsConfig.promptSourceFolders(configService, 'prompt'), [], 'Must read correct value.', ); @@ -184,7 +309,7 @@ suite('PromptsConfig', () => { suite('object', () => { test('empty', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({})), + PromptsConfig.promptSourceFolders(createMock({}), 'prompt'), ['.github/prompts'], 'Must read correct value.', ); @@ -206,7 +331,7 @@ suite('PromptsConfig', () => { '/var/logs/app.01.05.error': true, '.GitHub/prompts': true, './.tempfile': true, - })), + }), 'prompt'), [ '.github/prompts', '/root/.bashrc', @@ -254,7 +379,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '.github/prompts', '../assets/img/logo.v2.png', @@ -282,7 +407,7 @@ suite('PromptsConfig', () => { '/var/data/datafile.2025-02-05.json': '\n', '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '.github/prompts', ], @@ -317,7 +442,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER), - })), + }), 'prompt'), [ '../assets/img/logo.v2.png', '../.local/bin/script.sh', diff --git a/code/src/vs/platform/prompts/test/common/constants.test.ts b/code/src/vs/platform/prompts/test/common/constants.test.ts index 547cbab9087..3dcd0927f98 100644 --- a/code/src/vs/platform/prompts/test/common/constants.test.ts +++ b/code/src/vs/platform/prompts/test/common/constants.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { randomInt } from '../../../../base/common/numbers.js'; -import { getCleanPromptName, isPromptFile } from '../../common/constants.js'; +import { getCleanPromptName, isPromptOrInstructionsFile } from '../../common/constants.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; @@ -35,58 +35,54 @@ suite('Prompt Constants', () => { getCleanPromptName(URI.file('.github/copilot-instructions.md')), 'copilot-instructions', ); - }); - - test('• throws if not a prompt file URI provided', () => { - assert.throws(() => { - getCleanPromptName(URI.file('/path/to/default.prompt.md1')); - }); - - assert.throws(() => { - getCleanPromptName(URI.file('./some.md')); - }); + assert.strictEqual( + getCleanPromptName(URI.file('/etc/prompts/my-prompt')), + 'my-prompt', + ); - assert.throws(() => { - getCleanPromptName(URI.file('../some-folder/frequent.txt')); - }); + assert.strictEqual( + getCleanPromptName(URI.file('../some-folder/frequent.txt')), + 'frequent.txt', + ); - assert.throws(() => { - getCleanPromptName(URI.file('/etc/prompts/my-prompt')); - }); + assert.strictEqual( + getCleanPromptName(URI.parse('untitled:Untitled-1')), + 'Untitled-1', + ); }); }); - suite('• isPromptFile', () => { + suite('• isPromptOrInstructionsFile', () => { test('• returns `true` for prompt files', () => { assert( - isPromptFile(URI.file('/path/to/my-prompt.prompt.md')), + isPromptOrInstructionsFile(URI.file('/path/to/my-prompt.prompt.md')), ); assert( - isPromptFile(URI.file('../common.prompt.md')), + isPromptOrInstructionsFile(URI.file('../common.prompt.md')), ); assert( - isPromptFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)), + isPromptOrInstructionsFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)), ); assert( - isPromptFile(URI.file('.github/copilot-instructions.md')), + isPromptOrInstructionsFile(URI.file('.github/copilot-instructions.md')), ); }); test('• returns `false` for non-prompt files', () => { assert( - !isPromptFile(URI.file('/path/to/my-prompt.prompt.md1')), + !isPromptOrInstructionsFile(URI.file('/path/to/my-prompt.prompt.md1')), ); assert( - !isPromptFile(URI.file('../common.md')), + !isPromptOrInstructionsFile(URI.file('../common.md')), ); assert( - !isPromptFile(URI.file(`./some-${randomInt(1000)}.txt`)), + !isPromptOrInstructionsFile(URI.file(`./some-${randomInt(1000)}.txt`)), ); }); }); diff --git a/code/src/vs/platform/prompts/test/common/utils/mock.ts b/code/src/vs/platform/prompts/test/common/utils/mock.ts index a03543c6118..bb5f7ff7714 100644 --- a/code/src/vs/platform/prompts/test/common/utils/mock.ts +++ b/code/src/vs/platform/prompts/test/common/utils/mock.ts @@ -11,7 +11,7 @@ import { assertOneOf } from '../../../../../base/common/types.js'; * If you need to mock an `Service`, please use {@link mockService} * instead which provides better type safety guarantees for the case. * - * @throws Reading non-overidden property or function + * @throws Reading non-overridden property or function * on `TObject` throws an error. */ export function mockObject( @@ -53,7 +53,7 @@ type TAnyService = { * Same as more generic {@link mockObject} utility, but with * the service constraint on the `TService` type. * - * @throws Reading non-overidden property or function + * @throws Reading non-overridden property or function * on `TService` throws an error. */ export function mockService( diff --git a/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index 513641b4da5..d2c32d03774 100644 --- a/code/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/code/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/code/src/vs/platform/quickinput/browser/media/quickInput.css b/code/src/vs/platform/quickinput/browser/media/quickInput.css index e69e588b3fe..87b8a967b23 100644 --- a/code/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/code/src/vs/platform/quickinput/browser/media/quickInput.css @@ -72,7 +72,7 @@ .quick-input-header { cursor: grab; display: flex; - padding: 8px 6px 2px 6px; + padding: 6px 6px 2px 6px; } .quick-input-widget.hidden-input .quick-input-header { @@ -167,7 +167,8 @@ } .quick-input-widget.hidden-input .quick-input-list { - margin-top: 4px; /* reduce margins when input box hidden */ + margin-top: 4px; + /* reduce margins when input box hidden */ padding-bottom: 4px; } @@ -178,7 +179,7 @@ } .quick-input-list .monaco-scrollable-element { - padding: 0px 5px; + padding: 0px 6px; } .quick-input-list .quick-input-list-entry { @@ -212,9 +213,9 @@ flex: 1; } -.quick-input-list .quick-input-list-checkbox { +.quick-input-widget .monaco-checkbox { + margin-right: 0; align-self: center; - margin: 0; } .quick-input-list .quick-input-list-icon { @@ -239,24 +240,6 @@ margin-left: 5px; } -.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows { - margin-left: 10px; -} - -.quick-input-widget .quick-input-list .quick-input-list-checkbox { - display: none; -} -.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox { - display: inline; -} -.quick-input-widget.show-checkboxes .quick-input-list-entry.not-pickable { - margin-left: -10px; - - .quick-input-list-checkbox { - display: none; - } -} - .quick-input-list .quick-input-list-rows > .quick-input-list-row { display: flex; align-items: center; @@ -264,7 +247,8 @@ .quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label, .quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container { - flex: 1; /* make sure the icon label grows within the row */ + flex: 1; + /* make sure the icon label grows within the row */ } .quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon[class*='codicon-'] { @@ -276,7 +260,8 @@ } .quick-input-list .quick-input-list-entry .quick-input-list-entry-keybinding { - margin-right: 8px; /* separate from the separator label or scrollbar if any */ + margin-right: 8px; + /* separate from the separator label or scrollbar if any */ } .quick-input-list .quick-input-list-label-meta { @@ -299,7 +284,8 @@ } .quick-input-list .quick-input-list-entry .quick-input-list-separator { - margin-right: 4px; /* separate from keybindings or actions */ + margin-right: 4px; + /* separate from keybindings or actions */ } .quick-input-list .quick-input-list-entry-action-bar { @@ -326,7 +312,8 @@ } .quick-input-list .quick-input-list-entry-action-bar { - margin-right: 4px; /* separate from scrollbar */ + margin-right: 4px; + /* separate from scrollbar */ } .quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .action-label.always-visible, @@ -342,6 +329,7 @@ .quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator { color: inherit } + .quick-input-list .monaco-list-row.focused .monaco-keybinding-key { background: none; } diff --git a/code/src/vs/platform/quickinput/browser/quickInput.ts b/code/src/vs/platform/quickinput/browser/quickInput.ts index e55ae787e61..fcba3594b60 100644 --- a/code/src/vs/platform/quickinput/browser/quickInput.ts +++ b/code/src/vs/platform/quickinput/browser/quickInput.ts @@ -13,7 +13,7 @@ import { IInputBoxStyles } from '../../../base/browser/ui/inputbox/inputBox.js'; import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IListStyles } from '../../../base/browser/ui/list/listWidget.js'; import { IProgressBarStyles, ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js'; -import { IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; +import { Checkbox, IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; import { equals } from '../../../base/common/arrays.js'; import { TimeoutTimer } from '../../../base/common/async.js'; import { Codicon } from '../../../base/common/codicons.js'; @@ -103,7 +103,7 @@ export interface QuickInputUI { widget: HTMLElement; rightActionBar: ActionBar; inlineActionBar: ActionBar; - checkAll: HTMLInputElement; + checkAll: Checkbox; inputContainer: HTMLElement; filterContainer: HTMLElement; inputBox: QuickInputBox; @@ -1057,6 +1057,9 @@ export class QuickPick | undefined; private valueSelectionUpdated = true; private _placeholder: string | undefined; + private _ariaLabel: string | undefined; private _password = false; private _prompt: string | undefined; private readonly onDidValueChangeEmitter = this._register(new Emitter()); @@ -1199,6 +1203,15 @@ export class InputBox extends QuickInput implements IInputBox { this.update(); } + get ariaLabel() { + return this._ariaLabel; + } + + set ariaLabel(ariaLabel: string | undefined) { + this._ariaLabel = ariaLabel; + this.update(); + } + get password() { return this._password; } @@ -1269,6 +1282,20 @@ export class InputBox extends QuickInput implements IInputBox { if (this.ui.inputBox.password !== this.password) { this.ui.inputBox.password = this.password; } + let ariaLabel = this.ariaLabel; + // Only set aria label to the input box placeholder if we actually have an input box. + if (!ariaLabel && visibilities.inputBox) { + ariaLabel = this.placeholder + ? this.title + ? `${this.placeholder} - ${this.title}` + : this.placeholder + : this.title + ? this.title + : 'input'; + } + if (this.ui.inputBox.ariaLabel !== ariaLabel) { + this.ui.inputBox.ariaLabel = ariaLabel || 'input'; + } } } diff --git a/code/src/vs/platform/quickinput/browser/quickInputBox.ts b/code/src/vs/platform/quickinput/browser/quickInputBox.ts index ae8268ecc39..09032647c1f 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputBox.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputBox.ts @@ -100,6 +100,14 @@ export class QuickInputBox extends Disposable { this.findInput.setAdditionalToggles(toggles); } + get ariaLabel(): string { + return this.findInput.inputBox.inputElement.getAttribute('aria-label') || ''; + } + + set ariaLabel(ariaLabel: string) { + this.findInput.inputBox.inputElement.setAttribute('aria-label', ariaLabel); + } + hasFocus(): boolean { return this.findInput.inputBox.hasFocus(); } diff --git a/code/src/vs/platform/quickinput/browser/quickInputController.ts b/code/src/vs/platform/quickinput/browser/quickInputController.ts index 47b007a4193..d03d5b11b18 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputController.ts @@ -33,6 +33,8 @@ import { IConfigurationService } from '../../configuration/common/configuration. import { Platform, platform } from '../../../base/common/platform.js'; import { getWindowControlsStyle, WindowControlsStyle } from '../../window/common/window.js'; import { getZoomFactor } from '../../../base/browser/browser.js'; +import { Checkbox } from '../../../base/browser/ui/toggle/toggle.js'; +import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; const $ = dom.$; @@ -120,7 +122,7 @@ export class QuickInputController extends Disposable { } } - private getUI(showInActiveContainer?: boolean) { + private getUI(showInActiveContainer?: boolean): QuickInputUI { if (this.ui) { // In order to support aux windows, re-parent the controller // if the original event is from a different document @@ -152,14 +154,13 @@ export class QuickInputController extends Disposable { const headerContainer = dom.append(container, $('.quick-input-header')); - const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); - checkAll.type = 'checkbox'; - checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); - this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { + const checkAll = this._register(new Checkbox(localize('quickInput.checkAll', "Toggle all checkboxes"), false, { ...defaultCheckboxStyles, size: 15 })); + dom.append(headerContainer, checkAll.domNode); + this._register(checkAll.onChange(() => { const checked = checkAll.checked; list.setAllVisibleChecked(checked); })); - this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { + this._register(dom.addDisposableListener(checkAll.domNode, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space'... inputBox.setFocus(); } @@ -688,7 +689,7 @@ export class QuickInputController extends Disposable { ui.title.style.display = visibilities.title ? '' : 'none'; ui.description1.style.display = visibilities.description && (visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; ui.description2.style.display = visibilities.description && !(visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; - ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; + ui.checkAll.domNode.style.display = visibilities.checkAll ? '' : 'none'; ui.inputContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; @@ -706,16 +707,21 @@ export class QuickInputController extends Disposable { private setEnabled(enabled: boolean) { if (enabled !== this.enabled) { this.enabled = enabled; - for (const item of this.getUI().leftActionBar.viewItems) { + const ui = this.getUI(); + for (const item of ui.leftActionBar.viewItems) { (item as ActionViewItem).action.enabled = enabled; } - for (const item of this.getUI().rightActionBar.viewItems) { + for (const item of ui.rightActionBar.viewItems) { (item as ActionViewItem).action.enabled = enabled; } - this.getUI().checkAll.disabled = !enabled; - this.getUI().inputBox.enabled = enabled; - this.getUI().ok.enabled = enabled; - this.getUI().list.enabled = enabled; + if (enabled) { + ui.checkAll.enable(); + } else { + ui.checkAll.disable(); + } + ui.inputBox.enabled = enabled; + ui.ok.enabled = enabled; + ui.list.enabled = enabled; } } diff --git a/code/src/vs/platform/quickinput/browser/quickInputTree.ts b/code/src/vs/platform/quickinput/browser/quickInputTree.ts index 15c5eef04cc..bce6f79c693 100644 --- a/code/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/code/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -3,44 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from '../../../base/browser/dom.js'; import * as cssJs from '../../../base/browser/cssValue.js'; -import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from '../../../base/common/event.js'; +import * as dom from '../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js'; +import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; +import { AriaRole } from '../../../base/browser/ui/aria/aria.js'; +import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate.js'; +import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; +import { IIconLabelValueOptions, IconLabel } from '../../../base/browser/ui/iconLabel/iconLabel.js'; +import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IListVirtualDelegate } from '../../../base/browser/ui/list/list.js'; +import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; +import { Checkbox } from '../../../base/browser/ui/toggle/toggle.js'; +import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; -import { localize } from '../../../nls.js'; -import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { WorkbenchObjectTree } from '../../list/browser/listService.js'; -import { IThemeService } from '../../theme/common/themeService.js'; -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem, QuickPickFocus } from '../common/quickInput.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { equals } from '../../../base/common/arrays.js'; +import { ThrottledDelayer } from '../../../base/common/async.js'; +import { compareAnything } from '../../../base/common/comparers.js'; +import { memoize } from '../../../base/common/decorators.js'; +import { isCancellationError } from '../../../base/common/errors.js'; +import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from '../../../base/common/event.js'; import { IMatch } from '../../../base/common/filters.js'; -import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; -import { AriaRole } from '../../../base/browser/ui/aria/aria.js'; -import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js'; +import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from '../../../base/common/iconLabels.js'; import { KeyCode } from '../../../base/common/keyCodes.js'; -import { OS } from '../../../base/common/platform.js'; -import { memoize } from '../../../base/common/decorators.js'; -import { IIconLabelValueOptions, IconLabel } from '../../../base/browser/ui/iconLabel/iconLabel.js'; -import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; -import { isDark } from '../../theme/common/theme.js'; -import { URI } from '../../../base/common/uri.js'; -import { quickInputButtonToAction } from './quickInputUtils.js'; import { Lazy } from '../../../base/common/lazy.js'; -import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from '../../../base/common/iconLabels.js'; -import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; -import { compareAnything } from '../../../base/common/comparers.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; +import { observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; +import { OS } from '../../../base/common/platform.js'; import { escape, ltrim } from '../../../base/common/strings.js'; -import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; -import { ThrottledDelayer } from '../../../base/common/async.js'; -import { isCancellationError } from '../../../base/common/errors.js'; -import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; +import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; -import { observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; -import { equals } from '../../../base/common/arrays.js'; +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { WorkbenchObjectTree } from '../../list/browser/listService.js'; +import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; +import { isDark } from '../../theme/common/theme.js'; +import { IThemeService } from '../../theme/common/themeService.js'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickFocus, QuickPickItem } from '../common/quickInput.js'; +import { quickInputButtonToAction } from './quickInputUtils.js'; const $ = dom.$; @@ -67,8 +69,9 @@ interface IQuickPickElement extends IQuickInputItemLazyParts { interface IQuickInputItemTemplateData { entry: HTMLDivElement; - checkbox: HTMLInputElement; + checkbox: MutableDisposable; icon: HTMLDivElement; + outerLabel: HTMLElement; label: IconLabel; keybinding: KeybindingLabel; detail: IconLabel; @@ -320,7 +323,7 @@ abstract class BaseQuickInputListRenderer implement abstract templateId: string; constructor( - private readonly hoverDelegate: IHoverDelegate | undefined + private readonly hoverDelegate: IHoverDelegate | undefined, ) { } // TODO: only do the common stuff here and have a subclass handle their specific stuff @@ -332,13 +335,16 @@ abstract class BaseQuickInputListRenderer implement // Checkbox const label = dom.append(data.entry, $('label.quick-input-list-label')); + data.outerLabel = label; + data.checkbox = data.toDisposeTemplate.add(new MutableDisposable()); data.toDisposeTemplate.add(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { - if (!data.checkbox.offsetParent) { // If checkbox not visible: - e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740 + // `label` elements with role=checkboxes don't automatically toggle them like normal elements + if (data.checkbox.value && !e.defaultPrevented && data.checkbox.value.enabled) { + const checked = !data.checkbox.value.checked; + data.checkbox.value.checked = checked; + (data.element as QuickPickItemElement).checked = checked; } })); - data.checkbox = dom.append(label, $('input.quick-input-list-checkbox')); - data.checkbox.type = 'checkbox'; // Rows const rows = dom.append(label, $('.quick-input-list-rows')); @@ -402,14 +408,31 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer { - (data.element as QuickPickItemElement).checked = data.checkbox.checked; - })); + let checkbox = data.checkbox.value; + if (!checkbox) { + checkbox = new Checkbox(element.saneLabel, element.checked, { ...defaultCheckboxStyles, size: 15 }); + data.checkbox.value = checkbox; + data.outerLabel.prepend(checkbox.domNode); + } else { + checkbox.setTitle(element.saneLabel); + } - return data; + if (element.checkboxDisabled) { + checkbox.disable(); + } else { + checkbox.enable(); + } + + checkbox.checked = element.checked; + data.toDisposeElement.add(element.onChecked(checked => checkbox.checked = checked)); + data.toDisposeElement.add(checkbox.onChange(() => element.checked = checkbox.checked)); } renderElement(node: ITreeNode, index: number, data: IQuickInputItemTemplateData): void { @@ -421,9 +444,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer data.checkbox.checked = checked)); - data.checkbox.disabled = element.checkboxDisabled; + this.ensureCheckbox(element, data); const { labelHighlights, descriptionHighlights, detailHighlights } = element; @@ -557,12 +578,6 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer, index: number, data: IQuickInputItemTemplateData): void { const element = node.element; data.element = element; @@ -1089,7 +1104,7 @@ export class QuickInputTree extends Disposable { } const qpi = new QuickPickItemElement( index, - this._hasCheckboxes, + this._hasCheckboxes && item.pickable !== false, e => this._onButtonTriggered.fire(e), this._elementChecked, item, diff --git a/code/src/vs/platform/registry/common/platform.ts b/code/src/vs/platform/registry/common/platform.ts index b4a9bdd775e..9beaaf7c25c 100644 --- a/code/src/vs/platform/registry/common/platform.ts +++ b/code/src/vs/platform/registry/common/platform.ts @@ -48,6 +48,16 @@ class RegistryImpl implements IRegistry { public as(id: string): any { return this.data.get(id) || null; } + + public dispose() { + this.data.forEach((value) => { + if (Types.isFunction(value.dispose)) { + value.dispose(); + } + }); + this.data.clear(); + } + } export const Registry: IRegistry = new RegistryImpl(); diff --git a/code/src/vs/platform/terminal/common/capabilities/capabilities.ts b/code/src/vs/platform/terminal/common/capabilities/capabilities.ts index 17b2429ff8f..505e1f8ce46 100644 --- a/code/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/code/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -215,12 +215,14 @@ 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; readonly onCommandExecuted: Event; readonly onCommandInvalidated: Event; readonly onCurrentCommandInvalidated: Event; + readonly onPromptTypeChanged: Event; readonly onSetRichCommandDetection: Event; setContinuationPrompt(value: string): void; setPromptTerminator(value: string, lastPromptLine: string): void; @@ -242,6 +244,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/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 41efe327cfa..02415e9a3c3 100644 --- a/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/code/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -28,13 +28,15 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe protected _commands: TerminalCommand[] = []; private _cwd: string | undefined; private _promptTerminator: string | undefined; - private _currentCommand: PartialTerminalCommand = new PartialTerminalCommand(this._terminal); + private _currentCommand: PartialTerminalCommand; private _commandMarkers: IMarker[] = []; private _dimensions: ITerminalDimensions; private __isCommandStorageDisabled: boolean = false; 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; @@ -73,6 +75,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe readonly onCommandInvalidated = this._onCommandInvalidated.event; private readonly _onCurrentCommandInvalidated = this._register(new Emitter()); readonly onCurrentCommandInvalidated = this._onCurrentCommandInvalidated.event; + private readonly _onPromptTypeChanged = this._register(new Emitter()); + readonly onPromptTypeChanged = this._onPromptTypeChanged.event; private readonly _onSetRichCommandDetection = this._register(new Emitter()); readonly onSetRichCommandDetection = this._onSetRichCommandDetection.event; @@ -81,7 +85,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe @ILogService private readonly _logService: ILogService ) { super(); - + this._currentCommand = new PartialTerminalCommand(this._terminal); this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandStartChanged, this.onCommandExecuted, this._logService)); // Pull command line from the buffer if it was not set explicitly @@ -232,6 +236,11 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._onSetRichCommandDetection.fire(value); } + setPromptType(value: string): void { + this._promptType = value; + this._onPromptTypeChanged.fire(value); + } + setIsCommandStorageDisabled(): void { this.__isCommandStorageDisabled = true; } @@ -345,7 +354,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe handleCommandFinished(exitCode: number | undefined, options?: IHandleCommandOptions): void { // Command executed may not have happened yet, if not handle it now so the expected events - // properly propogate. This may cause the output to show up in the computed command line, + // properly propagate. This may cause the output to show up in the computed command line, // but the command line confidence will be low in the extension host for example and // therefore cannot be trusted anyway. if (!this._currentCommand.commandExecutedMarker) { @@ -531,24 +540,7 @@ class UnixPtyHeuristics extends Disposable { return; } - // Calculate the command - currentCommand.command = this._hooks.isCommandStorageDisabled ? '' : this._terminal.buffer.active.getLine(currentCommand.commandStartMarker.line)?.translateToString(true, currentCommand.commandStartX, currentCommand.commandRightPromptStartX).trim(); - let y = currentCommand.commandStartMarker.line + 1; - const commandExecutedLine = currentCommand.commandExecutedMarker.line; - for (; y < commandExecutedLine; y++) { - const line = this._terminal.buffer.active.getLine(y); - if (line) { - const continuation = currentCommand.continuations?.find(e => e.marker.line === y); - if (continuation) { - currentCommand.command += '\n'; - } - const startColumn = continuation?.end ?? 0; - currentCommand.command += line.translateToString(true, startColumn); - } - } - if (y === commandExecutedLine) { - currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, currentCommand.commandExecutedX) || ''; - } + currentCommand.command = this._capability.promptInputModel.ghostTextIndex > -1 ? this._capability.promptInputModel.value.substring(0, this._capability.promptInputModel.ghostTextIndex) : this._capability.promptInputModel.value; this._hooks.onCommandExecutedEmitter.fire(currentCommand as ITerminalCommand); } } diff --git a/code/src/vs/platform/terminal/common/terminal.ts b/code/src/vs/platform/terminal/common/terminal.ts index 3f326233dc7..a380ed4089e 100644 --- a/code/src/vs/platform/terminal/common/terminal.ts +++ b/code/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,51 @@ 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', + + + /** + * For zsh, we failed to set the sticky bit on the shell integration script folder. + */ + FailedToSetStickyBit = 'failedToSetStickyBit', + + /** + * For zsh, we failed to create a temp directory for the shell integration script. + */ + FailedToCreateTmpDir = 'failedToCreateTmpDir', +} + export enum TerminalExitReason { Unknown = 0, Shutdown = 1, diff --git a/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index f17dbb5be08..49ff56819b1 100644 --- a/code/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/code/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/code/src/vs/platform/terminal/node/ptyService.ts b/code/src/vs/platform/terminal/node/ptyService.ts index 840334c8d4c..87edbb57a7b 100644 --- a/code/src/vs/platform/terminal/node/ptyService.ts +++ b/code/src/vs/platform/terminal/node/ptyService.ts @@ -78,7 +78,7 @@ export class PtyService extends Disposable implements IPtyService { // #region Pty service contribution RPC calls - private readonly _autoRepliesContribution = new AutoRepliesPtyServiceContribution(this._logService); + private readonly _autoRepliesContribution: AutoRepliesPtyServiceContribution; @traceRpc async installAutoReply(match: string, reply: string) { await this._autoRepliesContribution.installAutoReply(match, reply); @@ -90,9 +90,7 @@ export class PtyService extends Disposable implements IPtyService { // #endregion - private readonly _contributions: IPtyServiceContribution[] = [ - this._autoRepliesContribution - ]; + private readonly _contributions: IPtyServiceContribution[]; private _lastPtyId: number = 0; @@ -148,6 +146,11 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore = this._register(new RequestStore(undefined, this._logService)); this._detachInstanceRequestStore.onCreateRequest(this._onDidRequestDetach.fire, this._onDidRequestDetach); + + this._autoRepliesContribution = new AutoRepliesPtyServiceContribution(this._logService); + + this._contributions = [this._autoRepliesContribution]; + } @traceRpc diff --git a/code/src/vs/platform/terminal/node/terminalEnvironment.ts b/code/src/vs/platform/terminal/node/terminalEnvironment.ts index e748e8ec88e..2acd35de838 100644 --- a/code/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/code/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'; @@ -28,23 +28,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 @@ -58,30 +64,32 @@ export async function getShellIntegrationInjection( logService: ILogService, productService: IProductService, skipStickyBit: boolean = false -): Promise { - // 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; +): Promise { + // 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' @@ -90,14 +98,16 @@ export async function getShellIntegrationInjection( if (options.shellIntegration.nonce) { envMixin['VSCODE_NONCE'] = options.shellIntegration.nonce; } + // Temporarily pass list of hardcoded env vars for shell env api + const scopedDownShellEnvs = ['PATH', 'VIRTUAL_ENV', 'HOME', 'SHELL', 'PWD']; if (shellLaunchConfig.shellIntegrationEnvironmentReporting) { if (isWindows) { const enableWindowsEnvReporting = options.windowsUseConptyDll || options.windowsEnableConpty && getWindowsBuildNumber() >= 22631 && shell !== 'bash.exe'; if (enableWindowsEnvReporting) { - envMixin['VSCODE_SHELL_ENV_REPORTING'] = '1'; + envMixin['VSCODE_SHELL_ENV_REPORTING'] = scopedDownShellEnvs.join(','); } } else { - envMixin['VSCODE_SHELL_ENV_REPORTING'] = '1'; + envMixin['VSCODE_SHELL_ENV_REPORTING'] = scopedDownShellEnvs.join(','); } } // Windows @@ -109,7 +119,7 @@ export async 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, ''); @@ -117,7 +127,7 @@ export async 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); @@ -127,15 +137,15 @@ export async 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 @@ -149,12 +159,12 @@ export async 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) { @@ -165,7 +175,7 @@ export async 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 @@ -174,7 +184,7 @@ export async 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)) { @@ -183,7 +193,7 @@ export async 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'; @@ -191,7 +201,7 @@ export async 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) { @@ -203,7 +213,7 @@ export async 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); @@ -237,18 +247,18 @@ export async function getShellIntegrationInjection( mkdirSync(zdotdir); } catch (err) { logService.error(`Failed to create zdotdir at ${zdotdir}: ${err}`); - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToCreateTmpDir }; } try { const chmodAsync = promisify(chmod); await chmodAsync(zdotdir, 0o1700); } catch { logService.error(`Failed to set sticky bit on ${zdotdir}: ${err}`); - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToSetStickyBit }; } } logService.error(`Failed to set sticky bit on ${zdotdir}: ${err}`); - return undefined; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToSetStickyBit }; } } envMixin['ZDOTDIR'] = zdotdir; @@ -271,11 +281,11 @@ export async 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/code/src/vs/platform/terminal/node/terminalProcess.ts b/code/src/vs/platform/terminal/node/terminalProcess.ts index 2ecdba352a6..2999fddce0a 100644 --- a/code/src/vs/platform/terminal/node/terminalProcess.ts +++ b/code/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 = await 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 = await 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. - } + } + 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 }); } + } 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/code/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/code/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index eb2d2ea96c7..be014833344 100644 --- a/code/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/code/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -11,7 +11,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 }; @@ -22,8 +22,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); @@ -31,27 +31,28 @@ function deepStrictEqualIgnoreStableVar(actual: IShellIntegrationConfigInjection suite('platform - terminalEnvironment', async () => { ensureNoDisposablesAreLeakedInTestSuite(); - suite('getShellIntegrationInjection', () => { - suite('should not enable', () => { + suite('getShellIntegrationInjection', async () => { + suite('should not enable', async () => { // 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', async () => { - ok(!(await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, defaultEnvironment, logService, productService, true))); - ok(await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, defaultEnvironment, logService, productService, true)); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'injection'); }); if (isWindows) { test('when on windows with conpty false', async () => { - ok(!(await getShellIntegrationInjection({ executable: pwshExe, args: ['-l'], isFeatureTerminal: false }, winptyProcessOptions, defaultEnvironment, logService, productService, true))); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: ['-l'], isFeatureTerminal: false }, winptyProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); } }); // These tests are only expected to work on Windows 10 build 18309 and above - (getWindowsBuildNumber() < 18309 ? suite.skip : suite)('pwsh', () => { + (getWindowsBuildNumber() < 18309 ? suite.skip : suite)('pwsh', async () => { const expectedPs1 = process.platform === 'win32' ? `try { . "${repoRoot}\\out\\vs\\workbench\\contrib\\terminal\\common\\scripts\\shellIntegration.ps1" } catch {}` : `. "${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1"`; - suite('should override args', () => { + suite('should override args', async () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '-noexit', '-command', @@ -65,7 +66,7 @@ suite('platform - terminalEnvironment', async () => { deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); }); - suite('when no logo', () => { + suite('when no logo', async () => { test('array - case insensitive', async () => { deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); @@ -80,8 +81,9 @@ suite('platform - terminalEnvironment', async () => { }); }); }); - suite('should incorporate login arg', () => { + suite('should incorporate login arg', async () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '-l', '-noexit', @@ -99,24 +101,24 @@ suite('platform - terminalEnvironment', async () => { deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); }); }); - suite('should not modify args', () => { + suite('should not modify args', async () => { test('when shell integration is disabled', async () => { - strictEqual(await getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); - strictEqual(await getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); - strictEqual(await getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); test('when using unrecognized arg', async () => { - strictEqual(await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); test('when using unrecognized arg (string)', async () => { - strictEqual(await getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); }); }); if (process.platform !== 'win32') { - suite('zsh', () => { - suite('should override args', () => { + suite('zsh', async () => { + suite('should override args', async () => { const username = userInfo().username; const expectedDir = new RegExp(`.+\/${username}-vscode-zsh`); const customZdotdir = '/custom/zsh/dotdir'; @@ -148,47 +150,48 @@ suite('platform - terminalEnvironment', async () => { ok(result.filesToCopy[3].source.match(expectedSources[3])); } test('when undefined, []', async () => { - const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true); + const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); - const result2 = await getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService, true); + const result2 = await getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService, true) as IShellIntegrationConfigInjection; deepStrictEqual(result2?.newArgs, ['-i']); assertIsEnabled(result2); }); - suite('should incorporate login arg', () => { + suite('should incorporate login arg', async () => { test('when array', async () => { - const result = await getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true); + const result = await getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true) as IShellIntegrationConfigInjection; deepStrictEqual(result?.newArgs, ['-il']); assertIsEnabled(result); }); }); - suite('should not modify args', () => { + suite('should not modify args', async () => { test('when shell integration is disabled', async () => { - strictEqual(await getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); - strictEqual(await getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); + strictEqual((await getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); test('when using unrecognized arg', async () => { - strictEqual(await getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); }); - suite('should incorporate global ZDOTDIR env variable', () => { + suite('should incorporate global ZDOTDIR env variable', async () => { test('when custom ZDOTDIR', async () => { - const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, { ...defaultEnvironment, ZDOTDIR: customZdotdir }, logService, productService, true); + const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, { ...defaultEnvironment, ZDOTDIR: customZdotdir }, logService, productService, true) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1, customZdotdir); }); test('when undefined', async () => { - const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, undefined, logService, productService, true); + const result1 = await getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, undefined, logService, productService, true) as IShellIntegrationConfigInjection; deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); }); }); }); }); - suite('bash', () => { - suite('should override args', () => { + suite('bash', async () => { + suite('should override args', async () => { test('when undefined, [], empty string', async () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '--init-file', `${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh` @@ -201,8 +204,9 @@ suite('platform - terminalEnvironment', async () => { deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); }); - suite('should set login env variable and not modify args', () => { + suite('should set login env variable and not modify args', async () => { const enabledExpectedResult = Object.freeze({ + type: 'injection', newArgs: [ '--init-file', `${repoRoot}/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh` @@ -216,13 +220,13 @@ suite('platform - terminalEnvironment', async () => { deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult); }); }); - suite('should not modify args', () => { + suite('should not modify args', async () => { test('when shell integration is disabled', async () => { - strictEqual(await getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); - strictEqual(await getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); + strictEqual((await getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); test('when custom array entry', async () => { - strictEqual(await getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true), undefined); + strictEqual((await getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure'); }); }); }); diff --git a/code/src/vs/platform/theme/browser/defaultStyles.ts b/code/src/vs/platform/theme/browser/defaultStyles.ts index 33ee6b363cd..1d61d3eec44 100644 --- a/code/src/vs/platform/theme/browser/defaultStyles.ts +++ b/code/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from '../../../base/browser/ui/button/button.js'; import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder } from '../common/colorRegistry.js'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder, checkboxDisabledBackground, checkboxDisabledForeground } from '../common/colorRegistry.js'; import { IProgressBarStyles } from '../../../base/browser/ui/progressbar/progressbar.js'; import { ICheckboxStyles, IToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; import { IDialogStyles } from '../../../base/browser/ui/dialog/dialog.js'; @@ -89,13 +89,11 @@ export function getToggleStyles(override: IStyleOverride): IToggl export const defaultCheckboxStyles: ICheckboxStyles = { checkboxBackground: asCssVariable(checkboxBackground), checkboxBorder: asCssVariable(checkboxBorder), - checkboxForeground: asCssVariable(checkboxForeground) + checkboxForeground: asCssVariable(checkboxForeground), + checkboxDisabledBackground: asCssVariable(checkboxDisabledBackground), + checkboxDisabledForeground: asCssVariable(checkboxDisabledForeground), }; -export function getCheckboxStyles(override: IStyleOverride): ICheckboxStyles { - return overrideStyles(override, defaultCheckboxStyles); -} - export const defaultDialogStyles: IDialogStyles = { dialogBackground: asCssVariable(editorWidgetBackground), dialogForeground: asCssVariable(editorWidgetForeground), diff --git a/code/src/vs/platform/theme/common/colorUtils.ts b/code/src/vs/platform/theme/common/colorUtils.ts index b1f28d96686..f55c8aad640 100644 --- a/code/src/vs/platform/theme/common/colorUtils.ts +++ b/code/src/vs/platform/theme/common/colorUtils.ts @@ -12,6 +12,7 @@ import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../j import * as platform from '../../registry/common/platform.js'; import { IColorTheme } from './themeService.js'; import * as nls from '../../../nls.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; // ------ API types @@ -50,7 +51,8 @@ export const enum ColorTransformType { Opaque, OneOf, LessProminent, - IfDefinedThenElse + IfDefinedThenElse, + Mix, } export type ColorTransform = @@ -60,7 +62,8 @@ export type ColorTransform = | { op: ColorTransformType.Opaque; value: ColorValue; background: ColorValue } | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number } - | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue }; + | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue } + | { op: ColorTransformType.Mix; color: ColorValue; with: ColorValue; ratio?: number }; export interface ColorDefaults { light: ColorValue | null; @@ -133,9 +136,9 @@ export interface IColorRegistry { type IJSONSchemaForColors = IJSONSchema & { properties: { [name: string]: { oneOf: [IJSONSchemaWithSnippets, IJSONSchema] } } }; type IJSONSchemaWithSnippets = IJSONSchema & { defaultSnippets: IJSONSchemaSnippet[] }; -class ColorRegistry implements IColorRegistry { +class ColorRegistry extends Disposable implements IColorRegistry { - private readonly _onDidChangeSchema = new Emitter(); + private readonly _onDidChangeSchema = this._register(new Emitter()); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; private colorsById: { [key: string]: ColorContribution }; @@ -143,6 +146,7 @@ class ColorRegistry implements IColorRegistry { private colorReferenceSchema: IJSONSchema & { enum: string[]; enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; constructor() { + super(); this.colorsById = {}; } @@ -214,7 +218,7 @@ class ColorRegistry implements IColorRegistry { return this.colorReferenceSchema; } - public toString() { + public override toString() { const sorter = (a: string, b: string) => { const cat1 = a.indexOf('.') === -1 ? 0 : 1; const cat2 = b.indexOf('.') === -1 ? 0 : 1; @@ -254,6 +258,12 @@ export function executeTransform(transform: ColorTransform, theme: IColorTheme): case ColorTransformType.Transparent: return resolveColorValue(transform.value, theme)?.transparent(transform.factor); + case ColorTransformType.Mix: { + const primaryColor = resolveColorValue(transform.color, theme) || Color.transparent; + const otherColor = resolveColorValue(transform.with, theme) || Color.transparent; + return primaryColor.mix(otherColor, transform.ratio); + } + case ColorTransformType.Opaque: { const backgroundColor = resolveColorValue(transform.background, theme); if (!backgroundColor) { diff --git a/code/src/vs/platform/theme/common/colors/inputColors.ts b/code/src/vs/platform/theme/common/colors/inputColors.ts index 1cf5a83e85a..f6a1348a6ae 100644 --- a/code/src/vs/platform/theme/common/colors/inputColors.ts +++ b/code/src/vs/platform/theme/common/colors/inputColors.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; // Import the effects we need import { Color, RGBA } from '../../../../base/common/color.js'; -import { registerColor, transparent, lighten, darken } from '../colorUtils.js'; +import { registerColor, transparent, lighten, darken, ColorTransformType } from '../colorUtils.js'; // Import the colors we need import { foreground, contrastBorder, focusBorder, iconForeground } from './baseColors.js'; @@ -193,6 +193,14 @@ export const checkboxSelectBorder = registerColor('checkbox.selectBorder', iconForeground, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); +export const checkboxDisabledBackground = registerColor('checkbox.disabled.background', + { op: ColorTransformType.Mix, color: checkboxBackground, with: checkboxForeground, ratio: 0.33 }, + nls.localize('checkbox.disabled.background', "Background of a disabled checkbox.")); + +export const checkboxDisabledForeground = registerColor('checkbox.disabled.foreground', + { op: ColorTransformType.Mix, color: checkboxForeground, with: checkboxBackground, ratio: 0.33 }, + nls.localize('checkbox.disabled.foreground', "Foreground of a disabled checkbox.")); + // ------ keybinding label diff --git a/code/src/vs/platform/theme/common/iconRegistry.ts b/code/src/vs/platform/theme/common/iconRegistry.ts index 88cee9c3ddb..9feac0c4ca4 100644 --- a/code/src/vs/platform/theme/common/iconRegistry.ts +++ b/code/src/vs/platform/theme/common/iconRegistry.ts @@ -14,6 +14,7 @@ import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../jsonschemas/common/jsonContributionRegistry.js'; import * as platform from '../../registry/common/platform.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; // ------ API types @@ -156,9 +157,9 @@ export const fontColorRegex = /^#[0-9a-fA-F]{0,6}$/; export const fontIdErrorMessage = localize('schema.fontId.formatError', 'The font ID must only contain letters, numbers, underscores and dashes.'); -class IconRegistry implements IIconRegistry { +class IconRegistry extends Disposable implements IIconRegistry { - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; private iconsById: { [key: string]: IconContribution }; @@ -182,6 +183,7 @@ class IconRegistry implements IIconRegistry { private iconFontsById: { [key: string]: IconFontDefinition }; constructor() { + super(); this.iconsById = {}; this.iconFontsById = {}; } @@ -263,7 +265,7 @@ class IconRegistry implements IIconRegistry { return this.iconFontsById[id]; } - public toString() { + public override toString() { const sorter = (i1: IconContribution, i2: IconContribution) => { return i1.id.localeCompare(i2.id); }; diff --git a/code/src/vs/platform/theme/common/themeService.ts b/code/src/vs/platform/theme/common/themeService.ts index 7911e0221da..fae8bf7f085 100644 --- a/code/src/vs/platform/theme/common/themeService.ts +++ b/code/src/vs/platform/theme/common/themeService.ts @@ -134,13 +134,14 @@ export interface IThemingRegistry { readonly onThemingParticipantAdded: Event; } -class ThemingRegistry implements IThemingRegistry { +class ThemingRegistry extends Disposable implements IThemingRegistry { private themingParticipants: IThemingParticipant[] = []; private readonly onThemingParticipantAddedEmitter: Emitter; constructor() { + super(); this.themingParticipants = []; - this.onThemingParticipantAddedEmitter = new Emitter(); + this.onThemingParticipantAddedEmitter = this._register(new Emitter()); } public onColorThemeChange(participant: IThemingParticipant): IDisposable { @@ -236,9 +237,3 @@ export interface IPartsSplash { windowBorderRadius: string | undefined; } | undefined; } - -export interface IPartsSplashWorkspaceOverride { - layoutInfo: { - auxiliarySideBarWidth: [number, string[] /* workspace identifier the override applies to */]; - }; -} diff --git a/code/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/code/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 6b7e07ce8f3..f3503dea201 100644 --- a/code/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/code/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -7,6 +7,7 @@ import { RunOnceScheduler } from '../../../base/common/async.js'; import { Color } from '../../../base/common/color.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IJSONSchema, IJSONSchemaMap } from '../../../base/common/jsonSchema.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; import * as nls from '../../../nls.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../jsonschemas/common/jsonContributionRegistry.js'; import * as platform from '../../registry/common/platform.js'; @@ -261,9 +262,9 @@ export interface ITokenClassificationRegistry { getTokenStylingSchema(): IJSONSchema; } -class TokenClassificationRegistry implements ITokenClassificationRegistry { +class TokenClassificationRegistry extends Disposable implements ITokenClassificationRegistry { - private readonly _onDidChangeSchema = new Emitter(); + private readonly _onDidChangeSchema = this._register(new Emitter()); readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; private currentTypeNumber = 0; @@ -347,6 +348,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { }; constructor() { + super(); this.tokenTypeById = Object.create(null); this.tokenModifierById = Object.create(null); this.typeHierarchy = Object.create(null); @@ -471,7 +473,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { } - public toString() { + public override toString() { const sorter = (a: string, b: string) => { const cat1 = a.indexOf('.') === -1 ? 0 : 1; const cat2 = b.indexOf('.') === -1 ? 0 : 1; diff --git a/code/src/vs/platform/theme/electron-main/themeMainService.ts b/code/src/vs/platform/theme/electron-main/themeMainService.ts index 2903217f74a..33ba510b4d3 100644 --- a/code/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/code/src/vs/platform/theme/electron-main/themeMainService.ts @@ -10,10 +10,10 @@ import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.j import { IConfigurationService } from '../../configuration/common/configuration.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStateService } from '../../state/node/state.js'; -import { IPartsSplash, IPartsSplashWorkspaceOverride } from '../common/themeService.js'; +import { IPartsSplash } from '../common/themeService.js'; import { IColorScheme } from '../../window/common/window.js'; import { ThemeTypeSelector } from '../common/theme.js'; -import { IBaseWorkspaceIdentifier } from '../../workspace/common/workspace.js'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { coalesce } from '../../../base/common/arrays.js'; import { getAllWindowsExcludingOffscreen } from '../../windows/electron-main/windows.js'; @@ -28,7 +28,9 @@ const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; const THEME_WINDOW_SPLASH_KEY = 'windowSplash'; -const THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY = 'windowSplashWorkspaceOverride'; +const THEME_WINDOW_SPLASH_OVERRIDE_KEY = 'windowSplashWorkspaceOverride'; + +const AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility'; namespace ThemeSettings { export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme'; @@ -36,6 +38,22 @@ namespace ThemeSettings { export const SYSTEM_COLOR_THEME = 'window.systemColorTheme'; } +interface IPartSplashOverrideWorkspaces { + [workspaceId: string]: { + sideBarVisible: boolean; + auxiliaryBarVisible: boolean; + }; +} + +interface IPartsSplashOverride { + layoutInfo: { + sideBarWidth: number; + auxiliaryBarWidth: number; + + workspaces: IPartSplashOverrideWorkspaces; + }; +} + export const IThemeMainService = createDecorator('themeMainService'); export interface IThemeMainService { @@ -46,8 +64,8 @@ export interface IThemeMainService { getBackgroundColor(): string; - saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void; - getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined; + saveWindowSplash(windowId: number | undefined, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): void; + getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined; getColorScheme(): IColorScheme; } @@ -56,10 +74,17 @@ export class ThemeMainService extends Disposable implements IThemeMainService { declare readonly _serviceBrand: undefined; + private static readonly DEFAULT_BAR_WIDTH = 300; + + private static readonly WORKSPACE_OVERRIDE_LIMIT = 50; + private readonly _onDidChangeColorScheme = this._register(new Emitter()); readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event; - constructor(@IStateService private stateService: IStateService, @IConfigurationService private configurationService: IConfigurationService) { + constructor( + @IStateService private stateService: IStateService, + @IConfigurationService private configurationService: IConfigurationService + ) { super(); // System Theme @@ -78,8 +103,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { private updateSystemColorTheme(): void { if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { - // only with `system` we can detect the system color scheme - electron.nativeTheme.themeSource = 'system'; + electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme } else { switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) { case 'dark': @@ -99,28 +123,34 @@ export class ThemeMainService extends Disposable implements IThemeMainService { electron.nativeTheme.themeSource = 'system'; break; } - } } getColorScheme(): IColorScheme { + + // high contrast is reflected by the shouldUseInvertedColorScheme property if (isWindows) { - // high contrast is reflected by the shouldUseInvertedColorScheme property if (electron.nativeTheme.shouldUseHighContrastColors) { // shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true }; } - } else if (isMacintosh) { - // high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set, reflecting the 'Invert colours' and `Increase contrast` settings in MacOS + } + + // high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set, + // reflecting the 'Invert colours' and `Increase contrast` settings in MacOS + else if (isMacintosh) { if (electron.nativeTheme.shouldUseInvertedColorScheme || electron.nativeTheme.shouldUseHighContrastColors) { return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: true }; } - } else if (isLinux) { - // ubuntu gnome seems to have 3 states, light dark and high contrast + } + + // ubuntu gnome seems to have 3 states, light dark and high contrast + else if (isLinux) { if (electron.nativeTheme.shouldUseHighContrastColors) { return { dark: true, highContrast: true }; } } + return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: false @@ -132,9 +162,11 @@ export class ThemeMainService extends Disposable implements IThemeMainService { if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && colorScheme.highContrast) { return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT; } + if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS; } + return undefined; } @@ -149,6 +181,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { return storedBackground; } } + // Otherwise we return the default background for the preferred base theme. If there's no preferred, use the stored one. switch (preferred ?? stored) { case ThemeTypeSelector.VS: return DEFAULT_BG_LIGHT; @@ -168,7 +201,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - saveWindowSplash(windowId: number | undefined, workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): void { + saveWindowSplash(windowId: number | undefined, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): void { // Update override as needed const splashOverride = this.updateWindowSplashOverride(workspace, splash); @@ -178,7 +211,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService { { key: THEME_STORAGE_KEY, data: splash.baseTheme }, { key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background }, { key: THEME_WINDOW_SPLASH_KEY, data: splash }, - splashOverride ? { key: THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, data: splashOverride } : undefined + splashOverride ? { key: THEME_WINDOW_SPLASH_OVERRIDE_KEY, data: splashOverride } : undefined ])); // Update in opened windows @@ -190,33 +223,89 @@ export class ThemeMainService extends Disposable implements IThemeMainService { this.updateSystemColorTheme(); } - private updateWindowSplashOverride(workspace: IBaseWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashWorkspaceOverride | undefined { - let splashOverride: IPartsSplashWorkspaceOverride | undefined = undefined; + private updateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashOverride | undefined { + let splashOverride: IPartsSplashOverride | undefined = undefined; let changed = false; if (workspace) { splashOverride = { ...this.getWindowSplashOverride() }; // make a copy for modifications - const [auxiliarySideBarWidth, workspaceIds] = splashOverride.layoutInfo.auxiliarySideBarWidth; - if (splash.layoutInfo?.auxiliarySideBarWidth) { - if (auxiliarySideBarWidth !== splash.layoutInfo.auxiliarySideBarWidth) { - splashOverride.layoutInfo.auxiliarySideBarWidth[0] = splash.layoutInfo.auxiliarySideBarWidth; - changed = true; - } + changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'sideBar'); + changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'auxiliaryBar') || changed; + } - if (!workspaceIds.includes(workspace.id)) { - workspaceIds.push(workspace.id); - changed = true; - } - } else { - const index = workspaceIds.indexOf(workspace.id); - if (index > -1) { - workspaceIds.splice(index, 1); - changed = true; - } + return changed ? splashOverride : undefined; + } + + private doUpdateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, splash: IPartsSplash, splashOverride: IPartsSplashOverride, part: 'sideBar' | 'auxiliaryBar'): boolean { + const currentWidth = part === 'sideBar' ? splash.layoutInfo?.sideBarWidth : splash.layoutInfo?.auxiliarySideBarWidth; + const overrideWidth = part === 'sideBar' ? splashOverride.layoutInfo.sideBarWidth : splashOverride.layoutInfo.auxiliaryBarWidth; + + // No layout info: remove override + let changed = false; + if (typeof currentWidth !== 'number') { + if (splashOverride.layoutInfo.workspaces[workspace.id]) { + delete splashOverride.layoutInfo.workspaces[workspace.id]; + changed = true; } + + return changed; } - return changed ? splashOverride : undefined; + let workspaceOverride = splashOverride.layoutInfo.workspaces[workspace.id]; + if (!workspaceOverride) { + const workspaceEntries = Object.keys(splashOverride.layoutInfo.workspaces); + if (workspaceEntries.length >= ThemeMainService.WORKSPACE_OVERRIDE_LIMIT) { + delete splashOverride.layoutInfo.workspaces[workspaceEntries[0]]; + changed = true; + } + + workspaceOverride = { sideBarVisible: false, auxiliaryBarVisible: false }; + splashOverride.layoutInfo.workspaces[workspace.id] = workspaceOverride; + changed = true; + } + + // Part has width: update width & visibility override + if (currentWidth > 0) { + if (overrideWidth !== currentWidth) { + splashOverride.layoutInfo[part === 'sideBar' ? 'sideBarWidth' : 'auxiliaryBarWidth'] = currentWidth; + changed = true; + } + + switch (part) { + case 'sideBar': + if (!workspaceOverride.sideBarVisible) { + workspaceOverride.sideBarVisible = true; + changed = true; + } + break; + case 'auxiliaryBar': + if (!workspaceOverride.auxiliaryBarVisible) { + workspaceOverride.auxiliaryBarVisible = true; + changed = true; + } + break; + } + } + + // Part is hidden: update visibility override + else { + switch (part) { + case 'sideBar': + if (workspaceOverride.sideBarVisible) { + workspaceOverride.sideBarVisible = false; + changed = true; + } + break; + case 'auxiliaryBar': + if (workspaceOverride.auxiliaryBarVisible) { + workspaceOverride.auxiliaryBarVisible = false; + changed = true; + } + break; + } + } + + return changed; } private updateBackgroundColor(windowId: number, splash: IPartsSplash): void { @@ -228,34 +317,81 @@ export class ThemeMainService extends Disposable implements IThemeMainService { } } - getWindowSplash(workspace: IBaseWorkspaceIdentifier | undefined): IPartsSplash | undefined { + getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined { const partSplash = this.stateService.getItem(THEME_WINDOW_SPLASH_KEY); if (!partSplash?.layoutInfo) { return partSplash; // return early: overrides currently only apply to layout info } - // Apply workspace specific overrides - let auxiliarySideBarWidthOverride: number | undefined; + const override = this.getWindowSplashOverride(); + + // Figure out side bar width based on workspace and overrides + let sideBarWidth: number; if (workspace) { - const [auxiliarySideBarWidth, workspaceIds] = this.getWindowSplashOverride().layoutInfo.auxiliarySideBarWidth; - if (workspaceIds.includes(workspace.id)) { - auxiliarySideBarWidthOverride = auxiliarySideBarWidth; + if (override.layoutInfo.workspaces[workspace.id]?.sideBarVisible === false) { + sideBarWidth = 0; + } else { + sideBarWidth = override.layoutInfo.sideBarWidth || partSplash.layoutInfo.sideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH; } + } else { + sideBarWidth = 0; + } + + // Figure out auxiliary bar width based on workspace, configuration and overrides + const auxiliarySideBarDefaultVisibility = this.configurationService.getValue(AUXILIARYBAR_DEFAULT_VISIBILITY); + let auxiliarySideBarWidth: number; + if (workspace) { + const auxiliaryBarVisible = override.layoutInfo.workspaces[workspace.id]?.auxiliaryBarVisible; + if (auxiliaryBarVisible === true) { + auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH; + } else if (auxiliaryBarVisible === false) { + auxiliarySideBarWidth = 0; + } else { + if (auxiliarySideBarDefaultVisibility === 'visible' || auxiliarySideBarDefaultVisibility === 'visibleInWorkspace') { + auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH; + } else { + auxiliarySideBarWidth = 0; + } + } + } else { + auxiliarySideBarWidth = 0; // technically not true if configured 'visible', but we never store splash per empty window, so we decide on a default here } return { ...partSplash, layoutInfo: { ...partSplash.layoutInfo, - // Only apply an auxiliary bar width when we have a workspace specific - // override. Auxiliary bar is not visible by default unless explicitly - // opened in a workspace. - auxiliarySideBarWidth: typeof auxiliarySideBarWidthOverride === 'number' ? auxiliarySideBarWidthOverride : 0 + sideBarWidth, + auxiliarySideBarWidth } }; } - private getWindowSplashOverride(): IPartsSplashWorkspaceOverride { - return this.stateService.getItem(THEME_WINDOW_SPLASH_WORKSPACE_OVERRIDE_KEY, { layoutInfo: { auxiliarySideBarWidth: [0, []] } }); + private getWindowSplashOverride(): IPartsSplashOverride { + let override = this.stateService.getItem(THEME_WINDOW_SPLASH_OVERRIDE_KEY); + + if (!override?.layoutInfo) { + override = { + layoutInfo: { + sideBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH, + auxiliaryBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH, + workspaces: {} + } + }; + } + + if (!override.layoutInfo.sideBarWidth) { + override.layoutInfo.sideBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH; + } + + if (!override.layoutInfo.auxiliaryBarWidth) { + override.layoutInfo.auxiliaryBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH; + } + + if (!override.layoutInfo.workspaces) { + override.layoutInfo.workspaces = {}; + } + + return override; } } diff --git a/code/src/vs/platform/update/electron-main/abstractUpdateService.ts b/code/src/vs/platform/update/electron-main/abstractUpdateService.ts index a1ec3fed95d..48d0d86a142 100644 --- a/code/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/code/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -231,5 +231,5 @@ export abstract class AbstractUpdateService implements IUpdateService { } protected abstract buildUpdateFeedUrl(quality: string): string | undefined; - protected abstract doCheckForUpdates(context: any): void; + protected abstract doCheckForUpdates(explicit: boolean): void; } diff --git a/code/src/vs/platform/update/electron-main/updateService.darwin.ts b/code/src/vs/platform/update/electron-main/updateService.darwin.ts index 57398fba4c8..b78ebc526fc 100644 --- a/code/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/code/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -91,8 +91,15 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return url; } - protected doCheckForUpdates(context: any): void { - this.setState(State.CheckingForUpdates(context)); + protected doCheckForUpdates(explicit: boolean): void { + if (!this.url) { + return; + } + + this.setState(State.CheckingForUpdates(explicit)); + + const url = explicit ? this.url : `${this.url}?bg=true`; + electron.autoUpdater.setFeedURL({ url }); electron.autoUpdater.checkForUpdates(); } diff --git a/code/src/vs/platform/update/electron-main/updateService.linux.ts b/code/src/vs/platform/update/electron-main/updateService.linux.ts index dd18900547d..8550ace2f43 100644 --- a/code/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/code/src/vs/platform/update/electron-main/updateService.linux.ts @@ -32,13 +32,15 @@ export class LinuxUpdateService extends AbstractUpdateService { return createUpdateURL(`linux-${process.arch}`, quality, this.productService); } - protected doCheckForUpdates(context: any): void { + protected doCheckForUpdates(explicit: boolean): void { if (!this.url) { return; } - this.setState(State.CheckingForUpdates(context)); - this.requestService.request({ url: this.url }, CancellationToken.None) + const url = explicit ? this.url : `${this.url}?bg=true`; + this.setState(State.CheckingForUpdates(explicit)); + + this.requestService.request({ url }, CancellationToken.None) .then(asJson) .then(update => { if (!update || !update.url || !update.version || !update.productVersion) { @@ -50,7 +52,7 @@ export class LinuxUpdateService extends AbstractUpdateService { .then(undefined, err => { this.logService.error(err); // only show message when explicitly checking for updates - const message: string | undefined = !!context ? (err.message || err) : undefined; + const message: string | undefined = explicit ? (err.message || err) : undefined; this.setState(State.Idle(UpdateType.Archive, message)); }); } diff --git a/code/src/vs/platform/update/electron-main/updateService.win32.ts b/code/src/vs/platform/update/electron-main/updateService.win32.ts index db92de2f198..8f92a3e9f35 100644 --- a/code/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/code/src/vs/platform/update/electron-main/updateService.win32.ts @@ -111,14 +111,15 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return createUpdateURL(platform, quality, this.productService); } - protected doCheckForUpdates(context: any): void { + protected doCheckForUpdates(explicit: boolean): void { if (!this.url) { return; } - this.setState(State.CheckingForUpdates(context)); + const url = explicit ? this.url : `${this.url}?bg=true`; + this.setState(State.CheckingForUpdates(explicit)); - this.requestService.request({ url: this.url }, CancellationToken.None) + this.requestService.request({ url }, CancellationToken.None) .then(asJson) .then(update => { const updateType = getUpdateType(); @@ -170,7 +171,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.logService.error(err); // only show message when explicitly checking for updates - const message: string | undefined = !!context ? (err.message || err) : undefined; + const message: string | undefined = explicit ? (err.message || err) : undefined; this.setState(State.Idle(getUpdateType(), message)); }); } diff --git a/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 54408a57aba..fcd19dbf3eb 100644 --- a/code/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/code/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/code/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts b/code/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts index 8c432401777..734c07abf07 100644 --- a/code/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts +++ b/code/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts @@ -7,13 +7,13 @@ import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { isPromptFile } from '../../../prompts/common/constants.js'; import { IStorageService } from '../../../storage/common/storage.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js'; import { IEnvironmentService } from '../../../environment/common/environment.js'; +import { isPromptOrInstructionsFile } from '../../../prompts/common/constants.js'; import { IUserDataProfile } from '../../../userDataProfile/common/userDataProfile.js'; import { IConfigurationService } from '../../../configuration/common/configuration.js'; import { areSame, IMergeResult as IPromptsMergeResult, merge } from './promptsMerge.js'; @@ -517,7 +517,7 @@ export class PromptsSynchronizer extends AbstractSynchroniser implements IUserDa for (const entry of stat.children || []) { const resource = entry.resource; - if (!isPromptFile(resource)) { + if (isPromptOrInstructionsFile(resource) === false) { continue; } diff --git a/code/src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts b/code/src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts index 9e7a7c87b17..a34db9ebc44 100644 --- a/code/src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts +++ b/code/src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts @@ -3,23 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/** - * Contains types from https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/ - */ +//#region Types -export interface AXProperty { - name: string; - value: AXValue; -} +import { URI } from '../../../base/common/uri.js'; export interface AXValue { - type: string; - value: any; + type: AXValueType; + value?: any; + relatedNodes?: AXNode[]; + sources?: AXValueSource[]; +} + +export interface AXValueSource { + type: AXValueSourceType; + value?: AXValue; + attribute?: string; + attributeValue?: string; + superseded?: boolean; + nativeSource?: AXValueNativeSourceType; + nativeSourceValue?: string; + invalid?: boolean; + invalidReason?: string; } export interface AXNode { nodeId: string; - ignored?: boolean; + ignored: boolean; ignoredReasons?: AXProperty[]; role?: AXValue; chromeRole?: AXValue; @@ -27,173 +36,450 @@ export interface AXNode { description?: AXValue; value?: AXValue; properties?: AXProperty[]; - parentId?: string; childIds?: string[]; backendDOMNodeId?: number; - frameId?: string; } -/** - * Converts an array of AXNode objects to a readable format. - * It processes the nodes to extract their text content, ignoring navigation elements and - * formatting them in a structured way. - * - * @remarks We can do more here, but this is a good start. - * @param axNodes - The array of AXNode objects to be converted to a readable format. - * @returns string - */ -export function convertToReadibleFormat(axNodes: AXNode[]): string { - if (!axNodes.length) { - return ''; - } - - const nodeMap = new Map(); - const processedNodes = new Set(); - const rootNodes: AXNode[] = []; - - // Build node map and identify root nodes - for (const node of axNodes) { - nodeMap.set(node.nodeId, node); - if (!node.parentId || !axNodes.some(n => n.nodeId === node.parentId)) { - rootNodes.push(node); - } - } - - function isNavigationElement(node: AXNode): boolean { - // Skip navigation and UI elements that don't contribute to content - const skipRoles = [ - 'navigation', - 'banner', - 'complementary', - 'toolbar', - 'menu', - 'menuitem', - 'tab', - 'tablist' - ]; - const skipTexts = [ - 'Skip to main content', - 'Toggle navigation', - 'Previous', - 'Next', - 'Copy', - 'Direct link to', - 'On this page', - 'Edit this page', - 'Search', - 'Command+K' - ]; - - const text = getNodeText(node); - const role = node.role?.value?.toString().toLowerCase() || ''; - // allow-any-unicode-next-line - return skipRoles.includes(role) || - skipTexts.some(skipText => text.includes(skipText)) || - text.startsWith('Direct link to') || - text.startsWith('\xAB ') || // Left-pointing double angle quotation mark - text.endsWith(' \xBB') || // Right-pointing double angle quotation mark - /^#\s*$/.test(text) || // Skip standalone # characters - text === '\u200B'; // Zero-width space character - } - - function getNodeText(node: AXNode): string { - const parts: string[] = []; - - // Add name if available - if (node.name?.value) { - parts.push(String(node.name.value)); - } - - // Add value if available and different from name - if (node.value?.value && node.value.value !== node.name?.value) { - parts.push(String(node.value.value)); - } - - // Add description if available and different from name and value - if (node.description?.value && - node.description.value !== node.name?.value && - node.description.value !== node.value?.value) { - parts.push(String(node.description.value)); - } - - return parts.join(' ').trim(); - } - - function isCodeBlock(node: AXNode): boolean { - return node.role?.value === 'code' || - (node.properties || []).some(p => p.name === 'code-block' || p.name === 'pre'); - } - - function processNode(node: AXNode, depth: number = 0, parentContext: { inCodeBlock: boolean; codeText: string[] } = { inCodeBlock: false, codeText: [] }): string[] { - if (!node || node.ignored || processedNodes.has(node.nodeId)) { - return []; - } +export interface AXProperty { + name: AXPropertyName; + value: AXValue; +} + +export type AXValueType = 'boolean' | 'tristate' | 'booleanOrUndefined' | 'idref' | 'idrefList' | 'integer' | 'node' | 'nodeList' | 'number' | 'string' | 'computedString' | 'token' | 'tokenList' | 'domRelation' | 'role' | 'internalRole' | 'valueUndefined'; - if (isNavigationElement(node)) { +export type AXValueSourceType = 'attribute' | 'implicit' | 'style' | 'contents' | 'placeholder' | 'relatedElement'; + +export type AXValueNativeSourceType = 'description' | 'figcaption' | 'label' | 'labelfor' | 'labelwrapped' | 'legend' | 'rubyannotation' | 'tablecaption' | 'title' | 'other'; + +export type AXPropertyName = 'url' | 'busy' | 'disabled' | 'editable' | 'focusable' | 'focused' | 'hidden' | 'hiddenRoot' | 'invalid' | 'keyshortcuts' | 'settable' | 'roledescription' | 'live' | 'atomic' | 'relevant' | 'root' | 'autocomplete' | 'hasPopup' | 'level' | 'multiselectable' | 'orientation' | 'multiline' | 'readonly' | 'required' | 'valuemin' | 'valuemax' | 'valuetext' | 'checked' | 'expanded' | 'pressed' | 'selected' | 'activedescendant' | 'controls' | 'describedby' | 'details' | 'errormessage' | 'flowto' | 'labelledby' | 'owns'; + +//#endregion + +interface AXNodeTree { + readonly node: AXNode; + readonly children: AXNodeTree[]; + parent: AXNodeTree | null; +} + +function createNodeTree(nodes: AXNode[]): AXNodeTree | null { + if (nodes.length === 0) { + return null; + } + + // Create a map of node IDs to their corresponding nodes for quick lookup + const nodeLookup = new Map(); + for (const node of nodes) { + nodeLookup.set(node.nodeId, node); + } + + // Helper function to get all non-ignored descendants of a node + function getNonIgnoredDescendants(nodeId: string): string[] { + const node = nodeLookup.get(nodeId); + if (!node || !node.childIds) { return []; } - processedNodes.add(node.nodeId); - const lines: string[] = []; - const text = getNodeText(node); - const currentIsCode = isCodeBlock(node); - const context = currentIsCode ? { inCodeBlock: true, codeText: [] } : parentContext; + const result: string[] = []; + for (const childId of node.childIds) { + const childNode = nodeLookup.get(childId); + if (!childNode) { + continue; + } - if (text) { - const indent = ' '.repeat(depth); - if (currentIsCode || context.inCodeBlock) { - // For code blocks, collect text without adding newlines - context.codeText.push(text.trim()); + if (childNode.ignored) { + // If child is ignored, add its non-ignored descendants instead + result.push(...getNonIgnoredDescendants(childId)); } else { - lines.push(indent + text); + // Otherwise, add the child itself + result.push(childId); } } + return result; + } + + // Create tree nodes only for non-ignored nodes + const nodeMap = new Map(); + for (const node of nodes) { + if (!node.ignored) { + nodeMap.set(node.nodeId, { node, children: [], parent: null }); + } + } + + // Establish parent-child relationships, bypassing ignored nodes + for (const node of nodes) { + if (node.ignored) { + continue; + } - // Process children + const treeNode = nodeMap.get(node.nodeId)!; if (node.childIds) { for (const childId of node.childIds) { - const child = nodeMap.get(childId); - if (child) { - const childLines = processNode(child, depth + 1, context); - lines.push(...childLines); + const childNode = nodeLookup.get(childId); + if (!childNode) { + continue; + } + + if (childNode.ignored) { + // If child is ignored, connect its non-ignored descendants to this node + const nonIgnoredDescendants = getNonIgnoredDescendants(childId); + for (const descendantId of nonIgnoredDescendants) { + const descendantTreeNode = nodeMap.get(descendantId); + if (descendantTreeNode) { + descendantTreeNode.parent = treeNode; + treeNode.children.push(descendantTreeNode); + } + } + } else { + // Normal case: add non-ignored child directly + const childTreeNode = nodeMap.get(childId); + if (childTreeNode) { + childTreeNode.parent = treeNode; + treeNode.children.push(childTreeNode); + } } } } + } - // If this is the root code block node, join all collected code text - if (currentIsCode && context.codeText.length > 0) { - const indent = ' '.repeat(depth); - lines.push(indent + context.codeText.join(' ')); + // Find the root node (a node without a parent) + for (const node of nodeMap.values()) { + if (!node.parent) { + return node; } + } + + return null; +} + +/** + * When possible, we will make sure lines are no longer than 80. This is to help + * certain pieces of software that can't handle long lines. + */ +const LINE_MAX_LENGTH = 80; - return lines; +/** + * Converts an accessibility tree represented by AXNode objects into a markdown string. + * + * @param uri The URI of the document + * @param axNodes The array of AXNode objects representing the accessibility tree + * @returns A markdown representation of the accessibility tree + */ +export function convertAXTreeToMarkdown(uri: URI, axNodes: AXNode[]): string { + const tree = createNodeTree(axNodes); + if (!tree) { + return ''; // Return empty string for empty tree } - // Process all nodes starting from roots - const allLines: string[] = []; - for (const node of rootNodes) { - const nodeLines = processNode(node); - if (nodeLines.length > 0) { - allLines.push(...nodeLines); + // Process tree to extract main content and navigation links + const mainContent = extractMainContent(uri, tree); + const navLinks = collectNavigationLinks(tree); + + // Combine main content and navigation links + return mainContent + (navLinks.length > 0 ? '\n\n## Additional Links\n' + navLinks.join('\n') : ''); +} + +function extractMainContent(uri: URI, tree: AXNodeTree): string { + const contentBuffer: string[] = []; + processNode(uri, tree, contentBuffer, 0, true); + return contentBuffer.join(''); +} + +function processNode(uri: URI, node: AXNodeTree, buffer: string[], depth: number, allowWrap: boolean): void { + const role = getNodeRole(node.node); + + switch (role) { + case 'navigation': + return; // Skip navigation nodes + + case 'heading': + processHeadingNode(uri, node, buffer, depth); + return; + + case 'paragraph': + processParagraphNode(uri, node, buffer, depth, allowWrap); + return; + + case 'list': + buffer.push('\n'); + for (const descChild of node.children) { + processNode(uri, descChild, buffer, depth + 1, true); + } + buffer.push('\n'); + return; + + case 'ListMarker': + // TODO: Should we normalize these ListMarkers to `-` and normal lists? + buffer.push(getNodeText(node.node, allowWrap)); + return; + + case 'listitem': { + const tempBuffer: string[] = []; + // Process the children of the list item + for (const descChild of node.children) { + processNode(uri, descChild, tempBuffer, depth + 1, true); + } + const indent = getLevel(node.node) > 1 ? ' '.repeat(getLevel(node.node)) : ''; + buffer.push(`${indent}${tempBuffer.join('').trim()}\n`); + return; + } + + case 'link': + if (!isNavigationLink(node)) { + const linkText = getNodeText(node.node, allowWrap); + const url = getLinkUrl(node.node); + if (!isSameUriIgnoringQueryAndFragment(uri, node.node)) { + buffer.push(`[${linkText}](${url})`); + } else { + buffer.push(linkText); + } + } + return; + case 'StaticText': { + const staticText = getNodeText(node.node, allowWrap); + if (staticText) { + buffer.push(staticText); + } + break; + } + case 'image': { + const altText = getNodeText(node.node, allowWrap) || 'Image'; + const imageUrl = getImageUrl(node.node); + if (imageUrl) { + buffer.push(`![${altText}](${imageUrl})\n\n`); + } else { + buffer.push(`[Image: ${altText}]\n\n`); + } + break; + } + + case 'DescriptionList': + processDescriptionListNode(uri, node, buffer, depth); + return; + + case 'blockquote': + buffer.push('> ' + getNodeText(node.node, allowWrap).replace(/\n/g, '\n> ') + '\n\n'); + break; + + // TODO: Is this the correct way to handle the generic role? + case 'generic': + buffer.push(' '); + break; + + case 'code': { + processCodeNode(uri, node, buffer, depth); + return; + } + + case 'pre': + buffer.push('```\n' + getNodeText(node.node, false) + '\n```\n\n'); + break; + + case 'table': + processTableNode(node, buffer); + return; + } + + // Process children if not already handled in specific cases + for (const child of node.children) { + processNode(uri, child, buffer, depth + 1, allowWrap); + } +} + +function getNodeRole(node: AXNode): string { + return node.role?.value as string || ''; +} + +function getNodeText(node: AXNode, allowWrap: boolean): string { + const text = node.name?.value as string || node.value?.value as string || ''; + if (!allowWrap) { + return text; + } + + if (text.length <= LINE_MAX_LENGTH) { + return text; + } + + const chars = text.split(''); + let lastSpaceIndex = -1; + for (let i = 1; i < chars.length; i++) { + if (chars[i] === ' ') { + lastSpaceIndex = i; + } + // Check if we reached the line max length, try to break at the last space + // before the line max length + if (i % LINE_MAX_LENGTH === 0 && lastSpaceIndex !== -1) { + // replace the space with a new line + chars[lastSpaceIndex] = '\n'; + lastSpaceIndex = i; } } + return chars.join(''); +} + +function getLevel(node: AXNode): number { + const levelProp = node.properties?.find(p => p.name === 'level'); + return levelProp ? Math.min(Number(levelProp.value.value) || 1, 6) : 1; +} + +function getLinkUrl(node: AXNode): string { + // Find URL in properties + const urlProp = node.properties?.find(p => p.name === 'url'); + return urlProp?.value.value as string || '#'; +} + +function getImageUrl(node: AXNode): string | null { + // Find URL in properties + const urlProp = node.properties?.find(p => p.name === 'url'); + return urlProp?.value.value as string || null; +} - // Process any remaining unprocessed nodes - for (const node of axNodes) { - if (!processedNodes.has(node.nodeId)) { - const nodeLines = processNode(node); - if (nodeLines.length > 0) { - allLines.push(...nodeLines); +function isNavigationLink(node: AXNodeTree): boolean { + // Check if this link is part of navigation + let current: AXNodeTree | null = node; + while (current) { + const role = getNodeRole(current.node); + if (['navigation', 'menu', 'menubar'].includes(role)) { + return true; + } + current = current.parent; + } + return false; +} + +function isSameUriIgnoringQueryAndFragment(uri: URI, node: AXNode): boolean { + // Check if this link is an anchor link + const link = getLinkUrl(node); + try { + const parsed = URI.parse(link); + return parsed.scheme === uri.scheme && parsed.authority === uri.authority && parsed.path === uri.path; + } catch (e) { + return false; + } +} + +function processParagraphNode(uri: URI, node: AXNodeTree, buffer: string[], depth: number, allowWrap: boolean): void { + buffer.push('\n'); + // Process the children of the paragraph + for (const child of node.children) { + processNode(uri, child, buffer, depth + 1, allowWrap); + } + buffer.push('\n\n'); +} + +function processHeadingNode(uri: URI, node: AXNodeTree, buffer: string[], depth: number): void { + buffer.push('\n'); + const level = getLevel(node.node); + buffer.push(`${'#'.repeat(level)} `); + // Process children nodes of the heading + for (const child of node.children) { + if (getNodeRole(child.node) === 'StaticText') { + buffer.push(getNodeText(child.node, false)); + } else { + processNode(uri, child, buffer, depth + 1, false); + } + } + buffer.push('\n\n'); +} + +function processDescriptionListNode(uri: URI, node: AXNodeTree, buffer: string[], depth: number): void { + buffer.push('\n'); + + // Process each child of the description list + for (const child of node.children) { + if (getNodeRole(child.node) === 'term') { + buffer.push('- **'); + // Process term nodes + for (const termChild of child.children) { + processNode(uri, termChild, buffer, depth + 1, true); } + buffer.push('** '); + } else if (getNodeRole(child.node) === 'definition') { + // Process description nodes + for (const descChild of child.children) { + processNode(uri, descChild, buffer, depth + 1, true); + } + buffer.push('\n'); } } - // Clean up empty lines and trim - return allLines - .filter((line, index, array) => { - // Keep the line if it's not empty or if it's not adjacent to another empty line - return line.trim() || (index > 0 && array[index - 1].trim()); - }) - .join('\n') - .trim(); + buffer.push('\n'); +} + +function processTableNode(node: AXNodeTree, buffer: string[]): void { + buffer.push('\n'); + + // Find rows + const rows = node.children.filter(child => getNodeRole(child.node).includes('row')); + + if (rows.length > 0) { + // First row as header + const headerCells = rows[0].children.filter(cell => getNodeRole(cell.node).includes('cell')); + + // Generate header row + const headerContent = headerCells.map(cell => getNodeText(cell.node, false) || ' '); + buffer.push('| ' + headerContent.join(' | ') + ' |\n'); + + // Generate separator row + buffer.push('| ' + headerCells.map(() => '---').join(' | ') + ' |\n'); + + // Generate data rows + for (let i = 1; i < rows.length; i++) { + const dataCells = rows[i].children.filter(cell => getNodeRole(cell.node).includes('cell')); + const rowContent = dataCells.map(cell => getNodeText(cell.node, false) || ' '); + buffer.push('| ' + rowContent.join(' | ') + ' |\n'); + } + } + + buffer.push('\n'); +} + +function processCodeNode(uri: URI, node: AXNodeTree, buffer: string[], depth: number): void { + const tempBuffer: string[] = []; + // Process the children of the code node + for (const child of node.children) { + processNode(uri, child, tempBuffer, depth + 1, false); + } + const isCodeblock = tempBuffer.some(text => text.includes('\n')); + if (isCodeblock) { + buffer.push('\n```\n'); + // Append the processed text to the buffer + buffer.push(tempBuffer.join('')); + buffer.push('\n```\n'); + } else { + buffer.push('`'); + let characterCount = 0; + // Append the processed text to the buffer + for (const tempItem of tempBuffer) { + characterCount += tempItem.length; + if (characterCount > LINE_MAX_LENGTH) { + buffer.push('\n'); + characterCount = 0; + } + buffer.push(tempItem); + buffer.push('`'); + } + } +} + +function collectNavigationLinks(tree: AXNodeTree): string[] { + const links: string[] = []; + collectLinks(tree, links); + return links; +} + +function collectLinks(node: AXNodeTree, links: string[]): void { + const role = getNodeRole(node.node); + + if (role === 'link' && isNavigationLink(node)) { + const linkText = getNodeText(node.node, true); + const url = getLinkUrl(node.node); + const description = node.node.description?.value as string || ''; + + links.push(`- [${linkText}](${url})${description ? ' - ' + description : ''}`); + } + + // Process children + for (const child of node.children) { + collectLinks(child, links); + } } diff --git a/code/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts b/code/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts index 0cbfd73e6d5..bd5adb42daf 100644 --- a/code/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts +++ b/code/src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts @@ -6,7 +6,7 @@ import { BrowserWindow } from 'electron'; import { IWebContentExtractorService } from '../common/webContentExtractor.js'; import { URI } from '../../../base/common/uri.js'; -import { AXNode, convertToReadibleFormat } from './cdpAccessibilityDomain.js'; +import { AXNode, convertAXTreeToMarkdown } from './cdpAccessibilityDomain.js'; import { Limiter } from '../../../base/common/async.js'; import { ResourceMap } from '../../../base/common/map.js'; @@ -60,7 +60,7 @@ export class NativeWebContentExtractorService implements IWebContentExtractorSer await win.loadURL(uri.toString(true)); win.webContents.debugger.attach('1.1'); const result: { nodes: AXNode[] } = await win.webContents.debugger.sendCommand('Accessibility.getFullAXTree'); - const str = convertToReadibleFormat(result.nodes); + const str = convertAXTreeToMarkdown(uri, result.nodes); this._webContentsCache.set(uri, { content: str, timestamp: Date.now() }); return str; } catch (err) { diff --git a/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts b/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts index 4aec8a15fba..61ae28d1e9b 100644 --- a/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts +++ b/code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts @@ -28,7 +28,7 @@ export class SharedWebContentExtractorService implements ISharedWebContentExtrac return undefined; } - const content = VSBuffer.wrap(new Uint8Array(await response.arrayBuffer())); + const content = VSBuffer.wrap(await response.bytes()); return content; } catch (err) { console.log(err); diff --git a/code/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts b/code/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts new file mode 100644 index 00000000000..f82ef6c0279 --- /dev/null +++ b/code/src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts @@ -0,0 +1,543 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from '../../../../base/common/uri.js'; +import { AXNode, AXProperty, AXValueType, convertAXTreeToMarkdown } from '../../electron-main/cdpAccessibilityDomain.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; + +suite('CDP Accessibility Domain', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const testUri = URI.parse('https://example.com/test'); + + function createAXValue(type: AXValueType, value: any) { + return { type, value }; + } + + function createAXProperty(name: string, value: any, type: AXValueType = 'string'): AXProperty { + return { + name: name as any, + value: createAXValue(type, value) + }; + } + + test('empty tree returns empty string', () => { + const result = convertAXTreeToMarkdown(testUri, []); + assert.strictEqual(result, ''); + }); + + //#region Heading Tests + + test('simple heading conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + childIds: ['node2'], + ignored: false, + role: createAXValue('role', 'heading'), + name: createAXValue('string', 'Test Heading'), + properties: [ + createAXProperty('level', 2, 'integer') + ] + }, + { + nodeId: 'node2', + childIds: [], + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Test Heading') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), '## Test Heading'); + }); + + //#endregion + + //#region Paragraph Tests + + test('paragraph with text conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'paragraph'), + childIds: ['node2'] + }, + { + nodeId: 'node2', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'This is a paragraph of text.') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), 'This is a paragraph of text.'); + }); + + test('really long paragraph should insert newlines at the space before 80 characters', () => { + const longStr = [ + 'This is a paragraph of text. It is really long. Like really really really really', + 'really really really really really really really long. That long.' + ]; + + const nodes: AXNode[] = [ + { + nodeId: 'node2', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', longStr.join(' ')) + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), longStr.join('\n')); + }); + + //#endregion + + //#region List Tests + + test('list conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'list'), + childIds: ['node2', 'node3'] + }, + { + nodeId: 'node2', + ignored: false, + role: createAXValue('role', 'listitem'), + childIds: ['node4', 'node6'] + }, + { + nodeId: 'node3', + ignored: false, + role: createAXValue('role', 'listitem'), + childIds: ['node5', 'node7'] + }, + { + nodeId: 'node4', + ignored: false, + role: createAXValue('role', 'ListMarker'), + name: createAXValue('string', '1. ') + }, + { + nodeId: 'node5', + ignored: false, + role: createAXValue('role', 'ListMarker'), + name: createAXValue('string', '2. ') + }, + { + nodeId: 'node6', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Item 1') + }, + { + nodeId: 'node7', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Item 2') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + const expected = + ` +1. Item 1 +2. Item 2 + +`; + assert.strictEqual(result, expected); + }); + + test('nested list conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'list1', + ignored: false, + role: createAXValue('role', 'list'), + childIds: ['item1', 'item2'] + }, + { + nodeId: 'item1', + ignored: false, + role: createAXValue('role', 'listitem'), + childIds: ['marker1', 'text1', 'nestedList'], + properties: [ + createAXProperty('level', 1, 'integer') + ] + }, + { + nodeId: 'marker1', + ignored: false, + role: createAXValue('role', 'ListMarker'), + name: createAXValue('string', '- ') + }, + { + nodeId: 'text1', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Item 1') + }, + { + nodeId: 'nestedList', + ignored: false, + role: createAXValue('role', 'list'), + childIds: ['nestedItem'] + }, + { + nodeId: 'nestedItem', + ignored: false, + role: createAXValue('role', 'listitem'), + childIds: ['nestedMarker', 'nestedText'], + properties: [ + createAXProperty('level', 2, 'integer') + ] + }, + { + nodeId: 'nestedMarker', + ignored: false, + role: createAXValue('role', 'ListMarker'), + name: createAXValue('string', '- ') + }, + { + nodeId: 'nestedText', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Item 1a') + }, + { + nodeId: 'item2', + ignored: false, + role: createAXValue('role', 'listitem'), + childIds: ['marker2', 'text2'], + properties: [ + createAXProperty('level', 1, 'integer') + ] + }, + { + nodeId: 'marker2', + ignored: false, + role: createAXValue('role', 'ListMarker'), + name: createAXValue('string', '- ') + }, + { + nodeId: 'text2', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Item 2') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + const indent = ' '; + const expected = + ` +- Item 1 +${indent}- Item 1a +- Item 2 + +`; + assert.strictEqual(result, expected); + }); + + //#endregion + + //#region Links Tests + + test('links conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'paragraph'), + childIds: ['node2'] + }, + { + nodeId: 'node2', + ignored: false, + role: createAXValue('role', 'link'), + name: createAXValue('string', 'Test Link'), + properties: [ + createAXProperty('url', 'https://test.com') + ] + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), '[Test Link](https://test.com)'); + }); + + test('links to same page are not converted to markdown links', () => { + const pageUri = URI.parse('https://example.com/page'); + const nodes: AXNode[] = [ + { + nodeId: 'link', + ignored: false, + role: createAXValue('role', 'link'), + name: createAXValue('string', 'Current page link'), + properties: [createAXProperty('url', 'https://example.com/page?section=1#header')] + } + ]; + + const result = convertAXTreeToMarkdown(pageUri, nodes); + assert.strictEqual(result.includes('Current page link'), true); + assert.strictEqual(result.includes('[Current page link]'), false); + }); + + //#endregion + + //#region Image Tests + + test('image conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'image'), + name: createAXValue('string', 'Alt text'), + properties: [ + createAXProperty('url', 'https://test.com/image.png') + ] + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), '![Alt text](https://test.com/image.png)'); + }); + + test('image without URL shows alt text', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'image'), + name: createAXValue('string', 'Alt text') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.trim(), '[Image: Alt text]'); + }); + + //#endregion + + //#region Description List Tests + + test('description list conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'dl', + ignored: false, + role: createAXValue('role', 'DescriptionList'), + childIds: ['term1', 'def1', 'term2', 'def2'] + }, + { + nodeId: 'term1', + ignored: false, + role: createAXValue('role', 'term'), + childIds: ['termText1'] + }, + { + nodeId: 'termText1', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Term 1') + }, + { + nodeId: 'def1', + ignored: false, + role: createAXValue('role', 'definition'), + childIds: ['defText1'] + }, + { + nodeId: 'defText1', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Definition 1') + }, + { + nodeId: 'term2', + ignored: false, + role: createAXValue('role', 'term'), + childIds: ['termText2'] + }, + { + nodeId: 'termText2', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Term 2') + }, + { + nodeId: 'def2', + ignored: false, + role: createAXValue('role', 'definition'), + childIds: ['defText2'] + }, + { + nodeId: 'defText2', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'Definition 2') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.includes('- **Term 1** Definition 1'), true); + assert.strictEqual(result.includes('- **Term 2** Definition 2'), true); + }); + + //#endregion + + //#region Blockquote Tests + + test('blockquote conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'blockquote'), + name: createAXValue('string', 'This is a blockquote\nWith multiple lines') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + const expected = + `> This is a blockquote +> With multiple lines`; + assert.strictEqual(result.trim(), expected); + }); + + //#endregion + + //#region Code Tests + + test('preformatted text conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'node1', + ignored: false, + role: createAXValue('role', 'pre'), + name: createAXValue('string', 'function test() {\n return true;\n}') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + const expected = + '```\nfunction test() {\n return true;\n}\n```'; + assert.strictEqual(result.trim(), expected); + }); + + test('code block conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'code', + ignored: false, + role: createAXValue('role', 'code'), + childIds: ['codeText'] + }, + { + nodeId: 'codeText', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'const x = 42;\nconsole.log(x);') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.includes('```'), true); + assert.strictEqual(result.includes('const x = 42;'), true); + assert.strictEqual(result.includes('console.log(x);'), true); + }); + + test('inline code conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'code', + ignored: false, + role: createAXValue('role', 'code'), + childIds: ['codeText'] + }, + { + nodeId: 'codeText', + ignored: false, + role: createAXValue('role', 'StaticText'), + name: createAXValue('string', 'const x = 42;') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + assert.strictEqual(result.includes('`const x = 42;`'), true); + }); + + //#endregion + + //#region Table Tests + + test('table conversion', () => { + const nodes: AXNode[] = [ + { + nodeId: 'table1', + ignored: false, + role: createAXValue('role', 'table'), + childIds: ['row1', 'row2'] + }, + { + nodeId: 'row1', + ignored: false, + role: createAXValue('role', 'row'), + childIds: ['cell1', 'cell2'] + }, + { + nodeId: 'row2', + ignored: false, + role: createAXValue('role', 'row'), + childIds: ['cell3', 'cell4'] + }, + { + nodeId: 'cell1', + ignored: false, + role: createAXValue('role', 'cell'), + name: createAXValue('string', 'Header 1') + }, + { + nodeId: 'cell2', + ignored: false, + role: createAXValue('role', 'cell'), + name: createAXValue('string', 'Header 2') + }, + { + nodeId: 'cell3', + ignored: false, + role: createAXValue('role', 'cell'), + name: createAXValue('string', 'Data 1') + }, + { + nodeId: 'cell4', + ignored: false, + role: createAXValue('role', 'cell'), + name: createAXValue('string', 'Data 2') + } + ]; + + const result = convertAXTreeToMarkdown(testUri, nodes); + const expected = + ` +| Header 1 | Header 2 | +| --- | --- | +| Data 1 | Data 2 | +`; + assert.strictEqual(result.trim(), expected.trim()); + }); + + //#endregion +}); diff --git a/code/src/vs/platform/window/common/window.ts b/code/src/vs/platform/window/common/window.ts index ddf6d46f7db..3d394c573d0 100644 --- a/code/src/vs/platform/window/common/window.ts +++ b/code/src/vs/platform/window/common/window.ts @@ -429,3 +429,4 @@ export function zoomLevelToZoomFactor(zoomLevel = 0): number { export const DEFAULT_WINDOW_SIZE = { width: 1200, height: 800 } as const; export const DEFAULT_AUX_WINDOW_SIZE = { width: 1024, height: 768 } as const; +export const DEFAULT_COMPACT_AUX_WINDOW_SIZE = { width: 640, height: 640 } as const; diff --git a/code/src/vs/platform/windows/electron-main/windowImpl.ts b/code/src/vs/platform/windows/electron-main/windowImpl.ts index 8e80df80519..ac4237ca0fa 100644 --- a/code/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/code/src/vs/platform/windows/electron-main/windowImpl.ts @@ -104,6 +104,9 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { private readonly _onDidLeaveFullScreen = this._register(new Emitter()); readonly onDidLeaveFullScreen = this._onDidLeaveFullScreen.event; + private readonly _onDidChangeAlwaysOnTop = this._register(new Emitter()); + readonly onDidChangeAlwaysOnTop = this._onDidChangeAlwaysOnTop.event; + //#endregion abstract readonly id: number; @@ -129,6 +132,7 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { })); this._register(Event.fromNodeEventEmitter(this._win, 'enter-full-screen')(() => this._onDidEnterFullScreen.fire())); this._register(Event.fromNodeEventEmitter(this._win, 'leave-full-screen')(() => this._onDidLeaveFullScreen.fire())); + this._register(Event.fromNodeEventEmitter(this._win, 'always-on-top-changed', (_, alwaysOnTop) => alwaysOnTop)(alwaysOnTop => this._onDidChangeAlwaysOnTop.fire(alwaysOnTop))); // Sheet Offsets const useCustomTitleStyle = !hasNativeTitlebar(this.configurationService, options?.titleBarStyle === 'hidden' ? TitlebarStyle.CUSTOM : undefined /* unknown */); @@ -146,23 +150,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 @@ -180,16 +174,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 @@ -705,7 +694,11 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this._register(this.workspacesManagementMainService.onDidDeleteUntitledWorkspace(e => this.onDidDeleteUntitledWorkspace(e))); // Inject headers when requests are incoming - const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; + const urls = ['https://*.vsassets.io/*']; + if (this.productService.extensionsGallery?.serviceUrl) { + const serviceUrl = URI.parse(this.productService.extensionsGallery.serviceUrl); + urls.push(`${serviceUrl.scheme}://${serviceUrl.authority}/*`); + } this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => { const headers = await this.getMarketplaceHeaders(); diff --git a/code/src/vs/platform/windows/electron-main/windows.ts b/code/src/vs/platform/windows/electron-main/windows.ts index 594cc7e4b28..0b92a4ab1f8 100644 --- a/code/src/vs/platform/windows/electron-main/windows.ts +++ b/code/src/vs/platform/windows/electron-main/windows.ts @@ -122,6 +122,7 @@ export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } export interface IDefaultBrowserWindowOptionsOverrides { forceNativeTitlebar?: boolean; disableFullscreen?: boolean; + alwaysOnTop?: boolean; } export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowState: IWindowState, overrides?: IDefaultBrowserWindowOptionsOverrides, webPreferences?: electron.WebPreferences): electron.BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } { @@ -209,6 +210,10 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt } } + if (overrides?.alwaysOnTop) { + options.alwaysOnTop = true; + } + return options; } diff --git a/code/src/vs/server/node/serverEnvironmentService.ts b/code/src/vs/server/node/serverEnvironmentService.ts index b3e97edc704..c06a700390b 100644 --- a/code/src/vs/server/node/serverEnvironmentService.ts +++ b/code/src/vs/server/node/serverEnvironmentService.ts @@ -40,6 +40,7 @@ export const serverOptions: OptionDescriptions> = { 'log': OPTIONS['log'], 'logsPath': OPTIONS['logsPath'], 'force-disable-user-env': OPTIONS['force-disable-user-env'], + 'enable-proposed-api': OPTIONS['enable-proposed-api'], /* ----- vs code web options ----- */ @@ -163,6 +164,7 @@ export interface ServerParsedArgs { 'logsPath'?: string; 'force-disable-user-env'?: boolean; + 'enable-proposed-api'?: string[]; /* ----- vs code web options ----- */ diff --git a/code/src/vs/server/node/webClientServer.ts b/code/src/vs/server/node/webClientServer.ts index 7d99f1d70f1..2c717fe803c 100644 --- a/code/src/vs/server/node/webClientServer.ts +++ b/code/src/vs/server/node/webClientServer.ts @@ -26,7 +26,7 @@ import { CancellationToken } from '../../base/common/cancellation.js'; import { URI } from '../../base/common/uri.js'; import { streamToBuffer } from '../../base/common/buffer.js'; import { IProductConfiguration } from '../../base/common/product.js'; -import { isString } from '../../base/common/types.js'; +import { isString, Mutable } from '../../base/common/types.js'; import { CharCode } from '../../base/common/charCode.js'; import { IExtensionManifest } from '../../platform/extensions/common/extensions.js'; import { ICSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; @@ -334,7 +334,7 @@ export class WebClientServer { scopes: [['user:email'], ['repo']] } : undefined; - const productConfiguration = { + const productConfiguration: Partial> = { embedderIdentifier: 'server-distro', extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? { ...this._productService.extensionsGallery, @@ -344,7 +344,13 @@ export class WebClientServer { path: `${webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined - } satisfies Partial; + }; + + const proposedApi = this._environmentService.args['enable-proposed-api']; + if (proposedApi?.length) { + productConfiguration.extensionsEnabledWithApiProposalVersion ??= []; + productConfiguration.extensionsEnabledWithApiProposalVersion.push(...proposedApi); + } if (!this._environmentService.isBuilt) { try { diff --git a/code/src/vs/workbench/api/browser/extensionHost.contribution.ts b/code/src/vs/workbench/api/browser/extensionHost.contribution.ts index 7a778aa030b..d5430634469 100644 --- a/code/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/code/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -88,6 +88,7 @@ import './mainThreadShare.js'; import './mainThreadProfileContentHandlers.js'; import './mainThreadAiRelatedInformation.js'; import './mainThreadAiEmbeddingVector.js'; +import './mainThreadAiSettingsSearch.js'; import './mainThreadMcp.js'; import './mainThreadChatStatus.js'; diff --git a/code/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts b/code/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts new file mode 100644 index 00000000000..8f7dde42d91 --- /dev/null +++ b/code/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { AiSettingsSearchResult, IAiSettingsSearchProvider, IAiSettingsSearchService } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostContext, ExtHostAiSettingsSearchShape, MainContext, MainThreadAiSettingsSearchShape, } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadAiSettingsSearch) +export class MainThreadAiSettingsSearch extends Disposable implements MainThreadAiSettingsSearchShape { + private readonly _proxy: ExtHostAiSettingsSearchShape; + private readonly _registrations = this._register(new DisposableMap()); + + constructor( + context: IExtHostContext, + @IAiSettingsSearchService private readonly _settingsSearchService: IAiSettingsSearchService, + ) { + super(); + this._proxy = context.getProxy(ExtHostContext.ExtHostAiSettingsSearch); + } + + $registerAiSettingsSearchProvider(handle: number): void { + const provider: IAiSettingsSearchProvider = { + searchSettings: (query, option, token) => { + return this._proxy.$startSearch(handle, query, option, token); + } + }; + this._registrations.set(handle, this._settingsSearchService.registerSettingsSearchProvider(provider)); + } + + $unregisterAiSettingsSearchProvider(handle: number): void { + this._registrations.deleteAndDispose(handle); + } + + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void { + if (!this._registrations.has(handle)) { + throw new Error(`No AI settings search provider found`); + } + + this._settingsSearchService.handleSearchResult(result); + } +} diff --git a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 0da8d2a09f1..4560cc6026f 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -21,6 +21,7 @@ import { ILanguageFeaturesService } from '../../../editor/common/services/langua import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../platform/log/common/log.js'; +import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IChatWidgetService } from '../../contrib/chat/browser/chat.js'; import { ChatInputPart } from '../../contrib/chat/browser/chatInputPart.js'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js'; @@ -29,7 +30,7 @@ import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../con import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js'; import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js'; import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; -import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../contrib/chat/common/constants.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; @@ -102,6 +103,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IExtensionService private readonly _extensionService: IExtensionService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -200,6 +202,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA slashCommands: [], disambiguation: [], locations: [ChatAgentLocation.Panel], // TODO all dynamic participants are panel only? + modes: [ChatMode.Ask] }, impl); } else { @@ -226,7 +229,19 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { - const revivedProgress = progress.kind === 'notebookEdit' ? ChatNotebookEdit.fromChatEdit(revive(progress)) : revive(progress) as IChatProgress; + + const revivedProgress = progress.kind === 'notebookEdit' + ? ChatNotebookEdit.fromChatEdit(revive(progress)) + : revive(progress) as IChatProgress; + + if (revivedProgress.kind === 'notebookEdit' + || revivedProgress.kind === 'textEdit' + || revivedProgress.kind === 'codeblockUri' + ) { + // make sure to use the canonical uri + revivedProgress.uri = this._uriIdentityService.asCanonicalUri(revivedProgress.uri); + } + if (revivedProgress.kind === 'progressTask') { const handle = ++this._responsePartHandlePool; const responsePartId = `${requestId}_${handle}`; diff --git a/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index 547e3346694..4f15773352b 100644 --- a/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/code/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -37,6 +37,8 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo requestId, codeBlocks: uiRequest.codeBlocks, chatRequestId: uiRequest.chatRequestId, + chatRequestModel: uiRequest.chatRequestModel, + chatSessionId: uiRequest.chatSessionId, location: uiRequest.location }; try { diff --git a/code/src/vs/workbench/api/browser/mainThreadComments.ts b/code/src/vs/workbench/api/browser/mainThreadComments.ts index b13b12de203..74890c18ebc 100644 --- a/code/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/code/src/vs/workbench/api/browser/mainThreadComments.ts @@ -93,9 +93,9 @@ export class MainThreadCommentThread implements languages.CommentThread { private readonly _onDidChangeCanReply = new Emitter(); get onDidChangeCanReply(): Event { return this._onDidChangeCanReply.event; } - set canReply(state: boolean) { + set canReply(state: boolean | languages.CommentAuthorInformation) { this._canReply = state; - this._onDidChangeCanReply.fire(this._canReply); + this._onDidChangeCanReply.fire(!!this._canReply); } get canReply() { @@ -182,7 +182,7 @@ export class MainThreadCommentThread implements languages.CommentThread { public resource: string, private _range: T | undefined, comments: languages.Comment[] | undefined, - private _canReply: boolean, + private _canReply: boolean | languages.CommentAuthorInformation, private _isTemplate: boolean, public editorId?: string ) { diff --git a/code/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/code/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index df4491d4e86..a484e898e01 100644 --- a/code/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/code/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -37,7 +37,7 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { private _forwardMarkers(resources: readonly URI[]): void { const data: [UriComponents, IMarkerData[]][] = []; for (const resource of resources) { - const allMarkerData = this._markerService.read({ resource }); + const allMarkerData = this._markerService.read({ resource, ignoreResourceFilters: true }); if (allMarkerData.length === 0) { data.push([resource, []]); } else { diff --git a/code/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts b/code/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts index db70e241abc..ae1198c5455 100644 --- a/code/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts +++ b/code/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts @@ -16,7 +16,7 @@ import { WorkspaceFolder } from '../../../platform/workspace/common/workspace.js class ExtHostEditSessionIdentityCreateParticipant implements IEditSessionIdentityCreateParticipant { private readonly _proxy: ExtHostWorkspaceShape; - private readonly timeout = 10000; + private readonly timeout = 20000; constructor(extHostContext: IExtHostContext) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); diff --git a/code/src/vs/workbench/api/browser/mainThreadEditors.ts b/code/src/vs/workbench/api/browser/mainThreadEditors.ts index a61ba3c7801..5bf9b358673 100644 --- a/code/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/code/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -397,10 +397,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } try { - const scmQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.isSCM); - const scmQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.label === scmQuickDiff?.label); + const primaryQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id); - return Promise.resolve(scmQuickDiffChanges.map(change => change.change) ?? []); + return Promise.resolve(primaryQuickDiffChanges.map(change => change.change) ?? []); } finally { quickDiffModelRef.dispose(); } diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 9aadd0d679a..9b5878d831d 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -614,7 +614,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.completionProvider.register(selector, provider)); } - $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined): void { + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined, eventHandle: number | undefined): void { const provider: languages.InlineCompletionsProvider = { provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); @@ -632,6 +632,22 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, + handleEndOfLifetime: async (completions, item, reason) => { + + function mapReason(reason: languages.InlineCompletionEndOfLifeReason, f: (reason: T1) => T2): languages.InlineCompletionEndOfLifeReason { + if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Ignored) { + return { + ...reason, + supersededBy: reason.supersededBy ? f(reason.supersededBy) : undefined, + }; + } + return reason; + } + + if (supportsHandleEvents) { + await this._proxy.$handleInlineCompletionEndOfLifetime(handle, completions.pid, item.idx, mapReason(reason, i => ({ pid: completions.pid, idx: i.idx }))); + } + }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { this._proxy.$freeInlineCompletionsList(handle, completions.pid); }, @@ -649,6 +665,19 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }, }; this._registrations.set(handle, this._languageFeaturesService.inlineCompletionsProvider.register(selector, provider)); + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChangeInlineCompletions = emitter.event; + } + } + + $emitInlineCompletionsChange(handle: number): void { + const obj = this._registrations.get(handle); + if (obj instanceof Emitter) { + obj.fire(undefined); + } } $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void { diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 67502d9db2c..8944b82e633 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -6,9 +6,9 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult, ToolProgress, toolResultHasBuffers, IToolProgressStep } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; -import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; +import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) @@ -16,7 +16,10 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre private readonly _proxy: ExtHostLanguageModelToolsShape; private readonly _tools = this._register(new DisposableMap()); - private readonly _countTokenCallbacks = new Map(); + private readonly _runningToolCalls = new Map(); constructor( extHostContext: IExtHostContext, @@ -32,7 +35,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre return Array.from(this._languageModelToolsService.getTools()); } - async $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise> { + async $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise | SerializableObjectWithBuffers>> { const result = await this._languageModelToolsService.invokeTool( dto, (input, token) => this._proxy.$countTokensForInvocation(dto.callId, input, token), @@ -40,31 +43,35 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre ); // Don't return extra metadata to EH - return { - content: result.content, - }; + const out: Dto = { content: result.content }; + return toolResultHasBuffers(result) ? new SerializableObjectWithBuffers(out) : out; + } + + $acceptToolProgress(callId: string, progress: IToolProgressStep): void { + this._runningToolCalls.get(callId)?.progress.report(progress); } $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { - const fn = this._countTokenCallbacks.get(callId); + const fn = this._runningToolCalls.get(callId); if (!fn) { throw new Error(`Tool invocation call ${callId} not found`); } - return fn(input, token); + return fn.countTokens(input, token); } $registerTool(id: string): void { const disposable = this._languageModelToolsService.registerToolImplementation( id, { - invoke: async (dto, countTokens, token) => { + invoke: async (dto, countTokens, progress, token) => { try { - this._countTokenCallbacks.set(dto.callId, countTokens); - const resultDto = await this._proxy.$invokeTool(dto, token); - return revive(resultDto) as IToolResult; + this._runningToolCalls.set(dto.callId, { countTokens, progress }); + const resultSerialized = await this._proxy.$invokeTool(dto, token); + const resultDto: Dto = resultSerialized instanceof SerializableObjectWithBuffers ? resultSerialized.value : resultSerialized; + return revive(resultDto); } finally { - this._countTokenCallbacks.delete(dto.callId); + this._runningToolCalls.delete(dto.callId); } }, prepareToolInvocation: (parameters, token) => this._proxy.$prepareToolInvocation(id, parameters, token), diff --git a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index cd7c6d9f883..bff5ae2f2a0 100644 --- a/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/code/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -6,6 +6,7 @@ import { AsyncIterableSource, DeferredPromise } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; @@ -153,7 +154,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { } this._logService.trace('[CHAT] request DONE', extension.value, requestId); } catch (err) { - this._logService.error('[CHAT] extension request ERRORED in STREAM', err, extension.value, requestId); + this._logService.error('[CHAT] extension request ERRORED in STREAM', toErrorMessage(err, true), extension.value, requestId); this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); } })(); @@ -163,7 +164,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._logService.debug('[CHAT] extension request DONE', extension.value, requestId); this._proxy.$acceptResponseDone(requestId, undefined); }, err => { - this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); + this._logService.error('[CHAT] extension request ERRORED', toErrorMessage(err, true), extension.value, requestId); this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); }); } diff --git a/code/src/vs/workbench/api/browser/mainThreadMcp.ts b/code/src/vs/workbench/api/browser/mainThreadMcp.ts index 0743d780c42..c93065c1b53 100644 --- a/code/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/code/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -5,18 +5,16 @@ 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 { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, 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 { ExtHostContext, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; +import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; +import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadMcp) export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @@ -24,21 +22,19 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { private _serverIdCounter = 0; private readonly _servers = new Map(); + private readonly _proxy: Proxied; private readonly _collectionDefinitions = this._register(new DisposableMap; 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); + const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); 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, @@ -46,7 +42,6 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { return proxy.$waitForInitialCollectionProviders(); }, canStart(collection, serverDefinition) { - // todo: SSE MPC servers without a remote authority could be served from the renderer if (collection.remoteAuthority !== _extHostContext.remoteAuthority) { return false; } @@ -78,25 +73,20 @@ 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, + resolveServerLanch: collection.canResolveLaunch ? (async def => { + const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); + return r ? McpServerLaunch.fromSerialized(r) : undefined; + }) : undefined, + remoteAuthority: this._extHostContext.remoteAuthority, + serverDefinitions, + }); this._collectionDefinitions.set(collection.id, { fromExtHost: collection, servers: serverDefinitions, - dispose: () => store.dispose(), + dispose: () => handle.dispose(), }); } } @@ -163,7 +153,11 @@ class ExtHostMcpServerLaunch extends Disposable implements IMcpMessageTransport } if (parsed) { - this._onDidReceiveMessage.fire(parsed); + if (Array.isArray(parsed)) { // streamable HTTP supports batching + parsed.forEach(p => this._onDidReceiveMessage.fire(p)); + } else { + this._onDidReceiveMessage.fire(parsed); + } } } diff --git a/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index d86233aabf9..2d15ed9a843 100644 --- a/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/code/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -23,13 +23,13 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickDiff); } - async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise { + async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, rootUri: UriComponents | undefined): Promise { const provider: QuickDiffProvider = { + id, label, rootUri: URI.revive(rootUri), selector, - isSCM: false, - visible, + kind: 'contributed', getOriginalResource: async (uri: URI) => { return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); } diff --git a/code/src/vs/workbench/api/browser/mainThreadSCM.ts b/code/src/vs/workbench/api/browser/mainThreadSCM.ts index 2e33d6f89a2..3ac52e3e71d 100644 --- a/code/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/code/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -16,7 +16,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; +import { IQuickDiffService } from '../../contrib/scm/common/quickDiff.js'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; @@ -235,7 +235,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { } } -class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { +class MainThreadSCMProvider implements ISCMProvider { private static ID_HANDLE = 0; private _id = `scm${MainThreadSCMProvider.ID_HANDLE++}`; @@ -287,8 +287,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get actionButton(): IObservable { return this._actionButton; } private _quickDiff: IDisposable | undefined; - public readonly isSCM: boolean = true; - public readonly visible: boolean = true; + private _stagedQuickDiff: IDisposable | undefined; private readonly _historyProvider = observableValue(this, undefined); get historyProvider() { return this._historyProvider; } @@ -335,17 +334,44 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { if (features.hasQuickDiffProvider && !this._quickDiff) { this._quickDiff = this._quickDiffService.addQuickDiffProvider({ + id: `${this._providerId}.quickDiffProvider`, label: features.quickDiffLabel ?? this.label, rootUri: this.rootUri, - isSCM: this.isSCM, - visible: this.visible, - getOriginalResource: (uri: URI) => this.getOriginalResource(uri) + kind: 'primary', + getOriginalResource: async (uri: URI) => { + if (!this.features.hasQuickDiffProvider) { + return null; + } + + const result = await this.proxy.$provideOriginalResource(this.handle, uri, CancellationToken.None); + return result && URI.revive(result); + } }); } else if (features.hasQuickDiffProvider === false && this._quickDiff) { this._quickDiff.dispose(); this._quickDiff = undefined; } + if (features.hasSecondaryQuickDiffProvider && !this._stagedQuickDiff) { + this._stagedQuickDiff = this._quickDiffService.addQuickDiffProvider({ + id: `${this._providerId}.secondaryQuickDiffProvider`, + label: features.secondaryQuickDiffLabel ?? this.label, + rootUri: this.rootUri, + kind: 'secondary', + getOriginalResource: async (uri: URI) => { + if (!this.features.hasSecondaryQuickDiffProvider) { + return null; + } + + const result = await this.proxy.$provideSecondaryOriginalResource(this.handle, uri, CancellationToken.None); + return result && URI.revive(result); + } + }); + } else if (features.hasSecondaryQuickDiffProvider === false && this._stagedQuickDiff) { + this._stagedQuickDiff.dispose(); + this._stagedQuickDiff = undefined; + } + if (features.hasHistoryProvider && !this.historyProvider.get()) { const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); this._historyProvider.set(historyProvider, undefined); @@ -490,6 +516,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } dispose(): void { + this._stagedQuickDiff?.dispose(); this._quickDiff?.dispose(); } } diff --git a/code/src/vs/workbench/api/browser/mainThreadSearch.ts b/code/src/vs/workbench/api/browser/mainThreadSearch.ts index 9f7772a2ffd..e8a72e6c59a 100644 --- a/code/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/code/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -14,6 +14,7 @@ import { ExtHostContext, ExtHostSearchShape, MainContext, MainThreadSearchShape import { revive } from '../../../base/common/marshalling.js'; import * as Constants from '../../contrib/search/common/constants.js'; import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; +import { AISearchKeyword } from '../../services/search/common/searchExtTypes.js'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -72,6 +73,16 @@ export class MainThreadSearch implements MainThreadSearchShape { provider.handleFindMatch(session, data); } + + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void { + const provider = this._searchProvider.get(handle); + if (!provider) { + throw new Error('Got result for unknown provider'); + } + + provider.handleKeywordResult(session, data); + } + $handleTelemetry(eventName: string, data: any): void { this._telemetryService.publicLog(eventName, data); } @@ -84,7 +95,8 @@ class SearchOperation { constructor( readonly progress?: (match: IFileMatch) => any, readonly id: number = ++SearchOperation._idPool, - readonly matches = new Map() + readonly matches = new Map(), + readonly keywords: AISearchKeyword[] = [] ) { // } @@ -104,6 +116,10 @@ class SearchOperation { this.progress?.(match); } + + addKeyword(result: AISearchKeyword): void { + this.keywords.push(result); + } } class RemoteSearchProvider implements ISearchResultProvider, IDisposable { @@ -153,7 +169,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { return Promise.resolve(searchP).then((result: ISearchCompleteStats) => { this._searches.delete(search.id); - return { results: Array.from(search.matches.values()), stats: result.stats, limitHit: result.limitHit, messages: result.messages }; + return { results: Array.from(search.matches.values()), aiKeywords: Array.from(search.keywords), stats: result.stats, limitHit: result.limitHit, messages: result.messages }; }, err => { this._searches.delete(search.id); return Promise.reject(err); @@ -183,7 +199,17 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { }); } - private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken): Promise { + handleKeywordResult(session: number, data: AISearchKeyword): void { + const searchOp = this._searches.get(session); + + if (!searchOp) { + // ignore... + return; + } + searchOp.addKeyword(data); + } + + private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { switch (query.type) { case QueryType.File: return this._proxy.$provideFileSearchResults(this._handle, session, query, token); diff --git a/code/src/vs/workbench/api/browser/mainThreadSpeech.ts b/code/src/vs/workbench/api/browser/mainThreadSpeech.ts index e2e0ad577a5..c8c7f103dec 100644 --- a/code/src/vs/workbench/api/browser/mainThreadSpeech.ts +++ b/code/src/vs/workbench/api/browser/mainThreadSpeech.ts @@ -98,7 +98,12 @@ export class MainThreadSpeech implements MainThreadSpeechShape { onDidChange: onDidChange.event, synthesize: async text => { await this.proxy.$synthesizeSpeech(session, text); - await raceCancellation(Event.toPromise(Event.filter(onDidChange.event, e => e.status === TextToSpeechStatus.Stopped)), token); + const disposable = new DisposableStore(); + try { + await raceCancellation(Event.toPromise(Event.filter(onDidChange.event, e => e.status === TextToSpeechStatus.Stopped, disposable), disposable), token); + } finally { + disposable.dispose(); + } } }; }, diff --git a/code/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/code/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index 2ce367773d7..b21653ed13c 100644 --- a/code/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/code/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/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 89b4f98c69e..7496427f1aa 100644 --- a/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -29,9 +29,7 @@ import { EditorResourceAccessor, SaveReason, SideBySideEditor } from '../../comm import { coalesce } from '../../../base/common/arrays.js'; import { ICanonicalUriService } from '../../../platform/workspace/common/canonicalUri.js'; import { revive } from '../../../base/common/marshalling.js'; -import { bufferToStream, readableToBuffer, VSBuffer } from '../../../base/common/buffer.js'; import { ITextFileService } from '../../services/textfile/common/textfiles.js'; -import { consumeStream } from '../../../base/common/stream.js'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -306,13 +304,15 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- encodings - async $decode(content: VSBuffer, resource: UriComponents | undefined, options?: { encoding: string }): Promise { - const stream = await this._textFileService.getDecodedStream(URI.revive(resource) ?? undefined, bufferToStream(content), { acceptTextOnly: true, encoding: options?.encoding }); - return consumeStream(stream, chunks => chunks.join()); + $resolveDecoding(resource: UriComponents | undefined, options?: { encoding: string }): Promise<{ preferredEncoding: string; guessEncoding: boolean; candidateGuessEncodings: string[] }> { + return this._textFileService.resolveDecoding(URI.revive(resource), options); } - async $encode(content: string, resource: UriComponents | undefined, options?: { encoding: string }): Promise { - const res = await this._textFileService.getEncodedReadable(URI.revive(resource) ?? undefined, content, { encoding: options?.encoding }); - return res instanceof VSBuffer ? res : readableToBuffer(res); + $validateDetectedEncoding(resource: UriComponents | undefined, detectedEncoding: string, options?: { encoding?: string }): Promise { + return this._textFileService.validateDetectedEncoding(URI.revive(resource), detectedEncoding, options); + } + + $resolveEncoding(resource: UriComponents | undefined, options?: { encoding: string }): Promise<{ encoding: string; addBOM: boolean }> { + return this._textFileService.resolveEncoding(URI.revive(resource), options); } } diff --git a/code/src/vs/workbench/api/common/configurationExtensionPoint.ts b/code/src/vs/workbench/api/common/configurationExtensionPoint.ts index 798f28ae3d4..64386dde6b0 100644 --- a/code/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/code/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -378,8 +378,8 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { inputs: [], servers: { 'mcp-server-time': { - command: 'python', - args: ['-m', 'mcp_server_time', '--local-timezone=America/Los_Angeles'] + command: 'uvx', + args: ['mcp_server_time', '--local-timezone=America/Los_Angeles'] } } }, diff --git a/code/src/vs/workbench/api/common/extHost.api.impl.ts b/code/src/vs/workbench/api/common/extHost.api.impl.ts index f13affa3aad..71f70cb8489 100644 --- a/code/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/code/src/vs/workbench/api/common/extHost.api.impl.ts @@ -27,7 +27,7 @@ import { ExtensionDescriptionRegistry } from '../../services/extensions/common/e import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; +import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2, AISearchKeyword } from '../../services/search/common/searchExtTypes.js'; import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from './extHost.protocol.js'; import { ExtHostRelatedInformation } from './extHostAiRelatedInformation.js'; import { ExtHostApiCommands } from './extHostApiCommands.js'; @@ -110,6 +110,7 @@ import { ExtHostWebviewPanels } from './extHostWebviewPanels.js'; import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; +import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -218,6 +219,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); + const extHostAiSettingsSearch = rpcProtocol.set(ExtHostContext.ExtHostAiSettingsSearch, new ExtHostAiSettingsSearch(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); @@ -917,9 +919,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'profileContentHandlers'); return extHostProfileContentHandlers.registerProfileContentHandler(extension, id, handler); }, - registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { + registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { checkProposedApiEnabled(extension, 'quickDiffProvider'); - return extHostQuickDiff.registerQuickDiffProvider(checkSelector(selector), quickDiffProvider, label, rootUri); + return extHostQuickDiff.registerQuickDiffProvider(extension, checkSelector(selector), quickDiffProvider, id, label, rootUri); }, get tabGroups(): vscode.TabGroups { return extHostEditorTabs.tabGroups; @@ -1037,9 +1039,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I let uriPromise: Thenable; options = (options ?? uriOrFileNameOrOptions) as ({ language?: string; content?: string; encoding?: string } | undefined); - if (typeof options?.encoding === 'string') { - checkProposedApiEnabled(extension, 'textDocumentEncoding'); - } if (typeof uriOrFileNameOrOptions === 'string') { uriPromise = Promise.resolve(URI.file(uriOrFileNameOrOptions)); @@ -1241,13 +1240,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'canonicalUriProvider'); return extHostWorkspace.provideCanonicalUri(uri, options, token); }, - decode(content: Uint8Array, uri: vscode.Uri | undefined, options?: { encoding: string }) { - checkProposedApiEnabled(extension, 'textDocumentEncoding'); - return extHostWorkspace.decode(content, uri, options); + decode(content: Uint8Array, options?: { uri?: vscode.Uri; encoding?: string }) { + return extHostWorkspace.decode(content, options); }, - encode(content: string, uri: vscode.Uri | undefined, options?: { encoding: string }) { - checkProposedApiEnabled(extension, 'textDocumentEncoding'); - return extHostWorkspace.encode(content, uri, options); + encode(content: string, options?: { uri?: vscode.Uri; encoding?: string }) { + return extHostWorkspace.encode(content, options); } }; @@ -1443,15 +1440,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerEmbeddingVectorProvider(model: string, provider: vscode.EmbeddingVectorProvider) { checkProposedApiEnabled(extension, 'aiRelatedInformation'); return extHostAiEmbeddingVector.registerEmbeddingVectorProvider(extension, model, provider); + }, + registerSettingsSearchProvider(provider: vscode.SettingsSearchProvider) { + checkProposedApiEnabled(extension, 'aiSettingsSearch'); + return extHostAiSettingsSearch.registerSettingsSearchProvider(extension, provider); } }; - // namespace: chat + // namespace: chatregisterMcpServerDefinitionProvider const chat: typeof vscode.chat = { - registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) { - checkProposedApiEnabled(extension, 'chatProvider'); - return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); - }, registerMappedEditsProvider(_selector: vscode.DocumentSelector, _provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); // no longer supported @@ -1524,18 +1521,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get tools() { return extHostLanguageModelTools.getTools(extension); }, - fileIsIgnored(uri: vscode.Uri, token: vscode.CancellationToken) { + fileIsIgnored(uri: vscode.Uri, token?: vscode.CancellationToken) { return extHostLanguageModels.fileIsIgnored(extension, uri, token); }, registerIgnoredFileProvider(provider: vscode.LanguageModelIgnoredFileProvider) { return extHostLanguageModels.registerIgnoredFileProvider(extension, provider); }, - registerMcpConfigurationProvider(id, provider) { + registerMcpServerDefinitionProvider(id, provider) { checkProposedApiEnabled(extension, 'mcpConfigurationProvider'); return extHostMcp.registerMcpConfigurationProvider(extension, id, provider); } }; + // todo@connor4312: proposed API back-compat + (lm as any).registerMcpConfigurationProvider = lm.registerMcpServerDefinitionProvider; + // namespace: speech const speech: typeof vscode.speech = { registerSpeechProvider(id: string, provider: vscode.SpeechProvider) { @@ -1788,7 +1788,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SpeechToTextStatus: extHostTypes.SpeechToTextStatus, TextToSpeechStatus: extHostTypes.TextToSpeechStatus, PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, + InlineCompletionEndOfLifeReasonKind: extHostTypes.InlineCompletionEndOfLifeReasonKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, + ChatImageMimeType: extHostTypes.ChatImageMimeType, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, @@ -1805,23 +1807,28 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatResponseMovePart: extHostTypes.ChatResponseMovePart, + ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, + ChatRequestTurn2: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, ChatRequestEditorData: extHostTypes.ChatRequestEditorData, ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, ChatReferenceBinaryData: extHostTypes.ChatReferenceBinaryData, + ChatRequestEditedFileEventKind: extHostTypes.ChatRequestEditedFileEventKind, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, LanguageModelToolResultPart: extHostTypes.LanguageModelToolResultPart, + LanguageModelToolResultPart2: extHostTypes.LanguageModelToolResultPart2, LanguageModelTextPart: extHostTypes.LanguageModelTextPart, LanguageModelToolCallPart: extHostTypes.LanguageModelToolCallPart, LanguageModelError: extHostTypes.LanguageModelError, LanguageModelToolResult: extHostTypes.LanguageModelToolResult, + LanguageModelToolResult2: extHostTypes.LanguageModelToolResult2, LanguageModelDataPart: extHostTypes.LanguageModelDataPart, - ChatImageMimeType: extHostTypes.ChatImageMimeType, + LanguageModelExtraDataPart: extHostTypes.LanguageModelExtraDataPart, ExtendedLanguageModelToolResult: extHostTypes.ExtendedLanguageModelToolResult, PreparedTerminalToolInvocation: extHostTypes.PreparedTerminalToolInvocation, LanguageModelChatToolMode: extHostTypes.LanguageModelChatToolMode, @@ -1834,10 +1841,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExcludeSettingOptions: ExcludeSettingOptions, TextSearchContext2: TextSearchContext2, TextSearchMatch2: TextSearchMatch2, + AISearchKeyword: AISearchKeyword, TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, ChatErrorLevel: extHostTypes.ChatErrorLevel, - McpSSEServerDefinition: extHostTypes.McpSSEServerDefinition, + McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, + SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind }; }; } diff --git a/code/src/vs/workbench/api/common/extHost.protocol.ts b/code/src/vs/workbench/api/common/extHost.protocol.ts index 51f8c0cfc3b..cd387657765 100644 --- a/code/src/vs/workbench/api/common/extHost.protocol.ts +++ b/code/src/vs/workbench/api/common/extHost.protocol.ts @@ -59,7 +59,7 @@ import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatPro import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; -import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolProgressStep, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js'; @@ -85,7 +85,8 @@ import { OutputChannelUpdateMode } from '../../services/output/common/output.js' import { CandidatePort } from '../../services/remote/common/tunnelModel.js'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js'; import * as search from '../../services/search/common/search.js'; -import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; +import { AISearchKeyword, TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; +import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -141,7 +142,7 @@ export type CommentThreadChanges = Partial<{ contextValue: string | null; comments: CommentChanges[]; collapseState: languages.CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | languages.CommentAuthorInformation; state: languages.CommentThreadState; applicability: languages.CommentThreadApplicability; isTemplate: boolean; @@ -474,8 +475,9 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; - $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined): void; + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined, eventHandle: number | undefined): void; $registerInlineEditProvider(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; + $emitInlineCompletionsChange(handle: number): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; $emitInlayHintsEvent(eventHandle: number): void; @@ -1379,7 +1381,8 @@ export type IToolDataDto = Omit; export interface MainThreadLanguageModelToolsShape extends IDisposable { $getTools(): Promise[]>; - $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise>; + $acceptToolProgress(callId: string, progress: IToolProgressStep): void; + $invokeTool(dto: IToolInvocation, token?: CancellationToken): Promise | SerializableObjectWithBuffers>>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $registerTool(id: string): void; $unregisterTool(name: string): void; @@ -1389,7 +1392,7 @@ export type IChatRequestVariableValueDto = Dto; export interface ExtHostLanguageModelToolsShape { $onDidChangeTools(tools: IToolDataDto[]): void; - $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise>; + $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise | SerializableObjectWithBuffers>>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; $prepareToolInvocation(toolId: string, parameters: any, token: CancellationToken): Promise; @@ -1479,8 +1482,9 @@ export interface MainThreadWorkspaceShape extends IDisposable { $unregisterEditSessionIdentityProvider(handle: number): void; $registerCanonicalUriProvider(handle: number, scheme: string): void; $unregisterCanonicalUriProvider(handle: number): void; - $decode(content: VSBuffer, resource: UriComponents | undefined, options?: { encoding?: string }): Promise; - $encode(content: string, resource: UriComponents | undefined, options?: { encoding?: string }): Promise; + $resolveDecoding(resource: UriComponents | undefined, options?: { encoding?: string }): Promise<{ preferredEncoding: string; guessEncoding: boolean; candidateGuessEncodings: string[] }>; + $validateDetectedEncoding(resource: UriComponents | undefined, detectedEncoding: string, options?: { encoding?: string }): Promise; + $resolveEncoding(resource: UriComponents | undefined, options?: { encoding?: string }): Promise<{ encoding: string; addBOM: boolean }>; } export interface IFileChangeDto { @@ -1522,6 +1526,7 @@ export interface MainThreadSearchShape extends IDisposable { $unregisterProvider(handle: number): void; $handleFileMatch(handle: number, session: number, data: UriComponents[]): void; $handleTextMatch(handle: number, session: number, data: search.IRawFileMatch2[]): void; + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void; $handleTelemetry(eventName: string, data: any): void; } @@ -1558,6 +1563,8 @@ export interface SCMProviderFeatures { hasHistoryProvider?: boolean; hasQuickDiffProvider?: boolean; quickDiffLabel?: string; + hasSecondaryQuickDiffProvider?: boolean; + secondaryQuickDiffLabel?: string; count?: number; commitTemplate?: string; acceptInputCommand?: languages.Command; @@ -1664,7 +1671,7 @@ export interface MainThreadSCMShape extends IDisposable { } export interface MainThreadQuickDiffShape extends IDisposable { - $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise; + $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string, rootUri: UriComponents | undefined): Promise; $unregisterQuickDiffProvider(handle: number): Promise; } @@ -1980,6 +1987,16 @@ export interface MainThreadAiRelatedInformationShape { $unregisterAiRelatedInformationProvider(handle: number): void; } +export interface ExtHostAiSettingsSearchShape { + $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise; +} + +export interface MainThreadAiSettingsSearchShape { + $registerAiSettingsSearchProvider(handle: number): void; + $unregisterAiSettingsSearchProvider(handle: number): void; + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void; +} + export interface ExtHostAiEmbeddingVectorShape { $provideAiEmbeddingVector(handle: number, strings: string[], token: CancellationToken): Promise; } @@ -2335,6 +2352,7 @@ export interface ExtHostLanguageFeaturesShape { $provideInlineEditsForRange(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; + $handleInlineCompletionEndOfLifetime(handle: number, pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void; $handleInlineCompletionRejection(handle: number, pid: number, idx: number): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; @@ -2529,6 +2547,7 @@ export interface ExtHostTerminalShellIntegrationShape { export interface ExtHostSCMShape { $provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; + $provideSecondaryOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; $onInputBoxValueChange(sourceControlHandle: number, value: string): void; $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise; $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; @@ -2970,6 +2989,7 @@ export interface ExtHostTestingShape { } export interface ExtHostMcpShape { + $resolveMcpLaunch(collectionId: string, label: string): Promise; $startMcp(id: number, launch: McpServerLaunch.Serialized): void; $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; @@ -2980,7 +3000,7 @@ export interface MainThreadMcpShape { $onDidChangeState(id: number, state: McpConnectionState): void; $onDidPublishLog(id: number, level: LogLevel, log: string): void; $onDidReceiveMessage(id: number, message: string): void; - $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: Dto[]): void; + $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void; $deleteMcpCollection(collectionId: string): void; } @@ -3061,7 +3081,7 @@ export interface MainThreadTestingShape { export type ChatStatusItemDto = { id: string; - title: string; + title: string | { label: string; link: string }; description: string; detail: string | undefined; }; @@ -3146,6 +3166,7 @@ export const MainContext = { MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), + MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), }; export const ExtHostContext = { @@ -3208,6 +3229,7 @@ export const ExtHostContext = { ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), + ExtHostAiSettingsSearch: createProxyIdentifier('ExtHostAiSettingsSearch'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), ExtHostTunnelService: createProxyIdentifier('ExtHostTunnelService'), ExtHostManagedSockets: createProxyIdentifier('ExtHostManagedSockets'), diff --git a/code/src/vs/workbench/api/common/extHostAiSettingsSearch.ts b/code/src/vs/workbench/api/common/extHostAiSettingsSearch.ts new file mode 100644 index 00000000000..c7c2d32f33d --- /dev/null +++ b/code/src/vs/workbench/api/common/extHostAiSettingsSearch.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { SettingsSearchProvider, SettingsSearchResult } from 'vscode'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { AiSettingsSearchProviderOptions } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostAiSettingsSearchShape, IMainContext, MainContext, MainThreadAiSettingsSearchShape } from './extHost.protocol.js'; +import { Disposable } from './extHostTypes.js'; +import { Progress } from '../../../platform/progress/common/progress.js'; +import { AiSettingsSearch } from './extHostTypeConverters.js'; + +export class ExtHostAiSettingsSearch implements ExtHostAiSettingsSearchShape { + private _settingsSearchProviders: Map = new Map(); + private _nextHandle = 0; + + private readonly _proxy: MainThreadAiSettingsSearchShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAiSettingsSearch); + } + + async $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise { + if (this._settingsSearchProviders.size === 0) { + throw new Error('No related information providers registered'); + } + + const provider = this._settingsSearchProviders.get(handle); + if (!provider) { + throw new Error('Settings search provider not found'); + } + + const progressReporter = new Progress((data) => { + this._proxy.$handleSearchResult(handle, AiSettingsSearch.fromSettingsSearchResult(data)); + }); + + return provider.provideSettingsSearchResults(query, option, progressReporter, token); + } + + registerSettingsSearchProvider(extension: IExtensionDescription, provider: SettingsSearchProvider): Disposable { + const handle = this._nextHandle; + this._nextHandle++; + this._settingsSearchProviders.set(handle, provider); + this._proxy.$registerAiSettingsSearchProvider(handle); + return new Disposable(() => { + this._proxy.$unregisterAiSettingsSearchProvider(handle); + this._settingsSearchProviders.delete(handle); + }); + } +} diff --git a/code/src/vs/workbench/api/common/extHostApiCommands.ts b/code/src/vs/workbench/api/common/extHostApiCommands.ts index 1d08ef21082..0dc498638b9 100644 --- a/code/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/code/src/vs/workbench/api/common/extHostApiCommands.ts @@ -547,6 +547,7 @@ const newCommands: ApiCommand[] = [ initialRange: v.initialRange ? typeConverters.Range.from(v.initialRange) : undefined, initialSelection: types.Selection.isSelection(v.initialSelection) ? typeConverters.Selection.from(v.initialSelection) : undefined, message: v.message, + attachments: v.attachments, autoSend: v.autoSend, position: v.position ? typeConverters.Position.from(v.position) : undefined, }; @@ -559,6 +560,7 @@ type InlineChatEditorApiArg = { initialRange?: vscode.Range; initialSelection?: vscode.Selection; message?: string; + attachments?: vscode.Uri[]; autoSend?: boolean; position?: vscode.Position; }; @@ -567,6 +569,7 @@ type InlineChatRunOptions = { initialRange?: IRange; initialSelection?: ISelection; message?: string; + attachments?: URI[]; autoSend?: boolean; position?: IPosition; }; diff --git a/code/src/vs/workbench/api/common/extHostChatAgents2.ts b/code/src/vs/workbench/api/common/extHostChatAgents2.ts index 09e03940e11..3e8fb508313 100644 --- a/code/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/code/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -13,7 +13,7 @@ import { Iterable } from '../../../base/common/iterator.js'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; import { StopWatch } from '../../../base/common/stopwatch.js'; -import { assertType } from '../../../base/common/types.js'; +import { assertType, isDefined } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Location } from '../../../editor/common/languages.js'; @@ -255,6 +255,7 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseConfirmationPart || part instanceof extHostTypes.ChatResponseCodeCitationPart || part instanceof extHostTypes.ChatResponseMovePart || + part instanceof extHostTypes.ChatResponseExtensionsPart || part instanceof extHostTypes.ChatResponseProgressPart2 ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -409,8 +410,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const model = await this.getModelForRequest(request, detector.extension); - const includeInteractionId = isProposedApiEnabled(detector.extension, 'chatParticipantPrivate'); - const extRequest = typeConvert.ChatAgentRequest.to(includeInteractionId ? request : { ...request, requestId: '' }, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request)); + const extRequest = typeConvert.ChatAgentRequest.to( + request, + location, + model, + this.getDiagnosticsWhenEnabled(detector.extension), + this.getToolsForRequest(detector.extension, request), + this.getTools2ForRequest(detector.extension, request), + detector.extension, + this._logService); return detector.provider.provideParticipantDetection( extRequest, @@ -494,13 +502,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); const model = await this.getModelForRequest(request, agent.extension); - const includeInteractionId = isProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); const extRequest = typeConvert.ChatAgentRequest.to( - includeInteractionId ? request : { ...request, requestId: '' }, + request, location, model, this.getDiagnosticsWhenEnabled(agent.extension), - this.getToolsForRequest(agent.extension, request) + this.getToolsForRequest(agent.extension, request), + this.getTools2ForRequest(agent.extension, request), + agent.extension, + this._logService ); inFlightRequest = { requestId: requestDto.requestId, extRequest }; this._inFlightRequests.add(inFlightRequest); @@ -560,12 +570,29 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return this._diagnostics.getDiagnostics(); } - private getToolsForRequest(extension: IExtensionDescription, request: Dto) { + private getTools2ForRequest(extension: IExtensionDescription, request: Dto): Map { + if (!request.userSelectedTools2) { + return new Map(); + } + const result = new Map(); + for (const tool of this._tools.getTools(extension)) { + if (typeof request.userSelectedTools2[tool.name] === 'boolean') { + result.set(tool.name, request.userSelectedTools2[tool.name]); + } + } + return result; + } + + private getToolsForRequest(extension: IExtensionDescription, request: Dto): vscode.ChatRequestToolSelection | undefined { if (!isNonEmptyArray(request.userSelectedTools)) { return undefined; } const selector = new Set(request.userSelectedTools); - return this._tools.getTools(extension).filter(candidate => selector.has(candidate.name)); + const tools = this._tools.getTools(extension).filter(candidate => selector.has(candidate.name)); + return { + tools, + isExclusive: request.toolSelectionIsExclusive, + }; } private async prepareHistoryTurns(extension: Readonly, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { @@ -579,12 +606,14 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS // REQUEST turn const varsWithoutTools = h.request.variables.variables - .filter(v => !v.isTool) - .map(v => typeConvert.ChatPromptReference.to(v, this.getDiagnosticsWhenEnabled(extension))); + .filter(v => v.kind !== 'tool') + .map(v => typeConvert.ChatPromptReference.to(v, this.getDiagnosticsWhenEnabled(extension), this._logService)) + .filter(isDefined); const toolReferences = h.request.variables.variables - .filter(v => v.isTool) + .filter(v => v.kind === 'tool') .map(typeConvert.ChatLanguageModelToolReference.to); - const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences); + const editedFileEvents = isProposedApiEnabled(extension, 'chatParticipantPrivate') ? h.request.editedFileEvents : undefined; + const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents); res.push(turn); // RESPONSE turn @@ -732,7 +761,7 @@ class ExtHostChatAgent { private _supportIssueReporting: boolean | undefined; private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; - private _welcomeMessageContent?: vscode.ChatWelcomeMessageContent | undefined; + private _additionalWelcomeMessage?: string | vscode.MarkdownString | undefined; private _titleProvider?: vscode.ChatTitleProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; private _pauseStateEmitter = new Emitter(); @@ -828,10 +857,7 @@ class ExtHostChatAgent { helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), supportIssueReporting: this._supportIssueReporting, requester: this._requester, - welcomeMessageContent: this._welcomeMessageContent && { - ...this._welcomeMessageContent, - message: typeConvert.MarkdownString.from(this._welcomeMessageContent.message), - } + additionalWelcomeMessage: (!this._additionalWelcomeMessage || typeof this._additionalWelcomeMessage === 'string') ? this._additionalWelcomeMessage : typeConvert.MarkdownString.from(this._additionalWelcomeMessage), }); updateScheduled = false; }); @@ -928,14 +954,14 @@ class ExtHostChatAgent { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); return that._welcomeMessageProvider; }, - set welcomeMessageContent(v) { + set additionalWelcomeMessage(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - that._welcomeMessageContent = v; + that._additionalWelcomeMessage = v; updateMetadataSoon(); }, - get welcomeMessageContent() { + get additionalWelcomeMessage() { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - return that._welcomeMessageContent; + return that._additionalWelcomeMessage; }, set titleProvider(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); diff --git a/code/src/vs/workbench/api/common/extHostChatStatus.ts b/code/src/vs/workbench/api/common/extHostChatStatus.ts index 38f22b171d7..47a85537313 100644 --- a/code/src/vs/workbench/api/common/extHostChatStatus.ts +++ b/code/src/vs/workbench/api/common/extHostChatStatus.ts @@ -49,10 +49,10 @@ export class ExtHostChatStatus { const item = Object.freeze({ id: id, - get title(): string { + get title(): string | { label: string; link: string } { return state.title; }, - set title(value: string) { + set title(value: string | { label: string; link: string }) { state.title = value; syncState(); }, diff --git a/code/src/vs/workbench/api/common/extHostCodeMapper.ts b/code/src/vs/workbench/api/common/extHostCodeMapper.ts index 5f573f995d9..5e22a066b8e 100644 --- a/code/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/code/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -52,6 +52,8 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape const request: vscode.MappedEditsRequest = { location: internalRequest.location, chatRequestId: internalRequest.chatRequestId, + chatRequestModel: internalRequest.chatRequestModel, + chatSessionId: internalRequest.chatSessionId, codeBlocks: internalRequest.codeBlocks.map(block => { return { code: block.code, diff --git a/code/src/vs/workbench/api/common/extHostCommands.ts b/code/src/vs/workbench/api/common/extHostCommands.ts index 0c170ec48a9..0bda2e82e46 100644 --- a/code/src/vs/workbench/api/common/extHostCommands.ts +++ b/code/src/vs/workbench/api/common/extHostCommands.ts @@ -32,6 +32,7 @@ import { IExtensionDescription } from '../../../platform/extensions/common/exten import { TelemetryTrustedValue } from '../../../platform/telemetry/common/telemetryUtils.js'; import { IExtHostTelemetry } from './extHostTelemetry.js'; import { generateUuid } from '../../../base/common/uuid.js'; +import { isCancellationError } from '../../../base/common/errors.js'; interface CommandHandler { callback: Function; @@ -256,7 +257,9 @@ export class ExtHostCommands implements ExtHostCommandsShape { id = actual.command; } } - this._logService.error(err, id, command.extension?.identifier); + if (!isCancellationError(err)) { + this._logService.error(err, id, command.extension?.identifier); + } if (!annotateError) { throw err; @@ -445,7 +448,6 @@ export class ApiCommandArgument { static readonly Selection = new ApiCommandArgument('selection', 'A selection in a text document', v => extHostTypes.Selection.isSelection(v), extHostTypeConverter.Selection.from); static readonly Number = new ApiCommandArgument('number', '', v => typeof v === 'number', v => v); static readonly String = new ApiCommandArgument('string', '', v => typeof v === 'string', v => v); - static readonly StringArray = ApiCommandArgument.Arr(ApiCommandArgument.String); static Arr(element: ApiCommandArgument) { return new ApiCommandArgument( diff --git a/code/src/vs/workbench/api/common/extHostComments.ts b/code/src/vs/workbench/api/common/extHostComments.ts index 908d094ec00..3f1ac7829cb 100644 --- a/code/src/vs/workbench/api/common/extHostComments.ts +++ b/code/src/vs/workbench/api/common/extHostComments.ts @@ -268,7 +268,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo contextValue: string | undefined; comments: vscode.Comment[]; collapsibleState: vscode.CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | vscode.CommentAuthorInformation; state: vscode.CommentThreadState; isTemplate: boolean; applicability: vscode.CommentThreadApplicability; @@ -316,9 +316,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return this._range; } - private _canReply: boolean = true; + private _canReply: boolean | vscode.CommentAuthorInformation = true; - set canReply(state: boolean) { + set canReply(state: boolean | vscode.CommentAuthorInformation) { if (this._canReply !== state) { this._canReply = state; this.modifications.canReply = state; @@ -465,7 +465,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo get collapsibleState() { return that.collapsibleState; }, set collapsibleState(value: vscode.CommentThreadCollapsibleState) { that.collapsibleState = value; }, get canReply() { return that.canReply; }, - set canReply(state: boolean) { that.canReply = state; }, + set canReply(state: boolean | vscode.CommentAuthorInformation) { that.canReply = state; }, get contextValue() { return that.contextValue; }, set contextValue(value: string | undefined) { that.contextValue = value; }, get label() { return that.label; }, diff --git a/code/src/vs/workbench/api/common/extHostExtensionService.ts b/code/src/vs/workbench/api/common/extHostExtensionService.ts index e6af1f781be..5b14b6d5a37 100644 --- a/code/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/code/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' && !modulePath?.endsWith('.cjs')); + } + 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/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b8122b07faa..9993c5c2112 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1350,6 +1350,7 @@ class InlineCompletionAdapter { && (typeof this._provider.handleDidShowCompletionItem === 'function' || typeof this._provider.handleDidPartiallyAcceptCompletionItem === 'function' || typeof this._provider.handleDidRejectCompletionItem === 'function' + || typeof this._provider.handleEndOfLifetime === 'function' ); } @@ -1428,6 +1429,10 @@ class InlineCompletionAdapter { completeBracketPairs: this._isAdditionsProposedApiEnabled ? item.completeBracketPairs : false, isInlineEdit: this._isAdditionsProposedApiEnabled ? item.isInlineEdit : false, showInlineEditMenu: this._isAdditionsProposedApiEnabled ? item.showInlineEditMenu : false, + displayLocation: (item.displayLocation && this._isAdditionsProposedApiEnabled) ? { + range: typeConvert.Range.from(item.displayLocation.range), + label: item.displayLocation.label, + } : undefined, warning: (item.warning && this._isAdditionsProposedApiEnabled) ? { message: typeConvert.MarkdownString.from(item.warning.message), icon: item.warning.icon ? typeConvert.IconPath.fromThemeIcon(item.warning.icon) : undefined, @@ -1555,6 +1560,16 @@ class InlineCompletionAdapter { } } + handleEndOfLifetime(pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void { + const completionItem = this._references.get(pid)?.items[idx]; + if (completionItem) { + if (this._provider.handleEndOfLifetime && this._isAdditionsProposedApiEnabled) { + const r = typeConvert.InlineCompletionEndOfLifeReason.to(reason, ref => this._references.get(ref.pid)?.items[ref.idx]); + this._provider.handleEndOfLifetime(completionItem, r); + } + } + } + handleRejection(pid: number, idx: number): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { @@ -2704,8 +2719,16 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- ghost text registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider, metadata: vscode.InlineCompletionItemProviderMetadata | undefined): vscode.Disposable { + const eventHandle = typeof provider.onDidChange === 'function' && isProposedApiEnabled(extension, 'inlineCompletionsAdditions') ? this._nextHandle() : undefined; const adapter = new InlineCompletionAdapter(extension, this._documents, provider, this._commands.converter); const handle = this._addNewAdapter(adapter, extension); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChange!(_ => this._proxy.$emitInlineCompletionsChange(eventHandle)); + result = Disposable.from(result, subscription); + } + this._proxy.$registerInlineCompletionsSupport( handle, this._transformDocumentSelector(selector, extension), @@ -2714,8 +2737,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF metadata?.yieldTo?.map(extId => ExtensionIdentifier.toKey(extId)) || [], metadata?.displayName, metadata?.debounceDelayMs, + eventHandle, ); - return this._createDisposable(handle); + return result; } $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { @@ -2738,6 +2762,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } + $handleInlineCompletionEndOfLifetime(handle: number, pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + adapter.handleEndOfLifetime(pid, idx, reason); + }, undefined, undefined); + } + $handleInlineCompletionRejection(handle: number, pid: number, idx: number): void { this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.handleRejection(pid, idx); diff --git a/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 0e7b81d42cf..c0a1bec3198 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,13 +12,45 @@ import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/editFileTool.js'; +import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/tools.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; -import * as typeConvert from './extHostTypeConverters.js'; -import { InternalFetchWebPageToolId, IToolInputProcessor } from '../../contrib/chat/common/tools/tools.js'; -import { EditToolData, InternalEditToolId, EditToolInputProcessor, ExtensionEditToolId } from '../../contrib/chat/common/tools/editFileTool.js'; -import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostLanguageModels } from './extHostLanguageModels.js'; +import * as typeConvert from './extHostTypeConverters.js'; +import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; + +class Tool { + + private _data: IToolDataDto; + private _apiObject: vscode.LanguageModelToolInformation | undefined; + + constructor(data: IToolDataDto) { + this._data = data; + } + + update(newData: IToolDataDto): void { + this._data = newData; + } + + get data(): IToolDataDto { + return this._data; + } + + get apiObject(): vscode.LanguageModelToolInformation { + if (!this._apiObject) { + const that = this; + this._apiObject = Object.freeze({ + get name() { return that._data.id; }, + get description() { return that._data.modelDescription; }, + get inputSchema() { return that._data.inputSchema; }, + get tags() { return that._data.tags ?? []; }, + }); + } + return this._apiObject; + } +} export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -27,9 +59,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape private readonly _tokenCountFuncs = new Map Thenable>(); /** A map of all known tools, from other EHs or registered in vscode core */ - private readonly _allTools = new Map(); - - private readonly _toolInputProcessors = new Map(); + private readonly _allTools = new Map(); constructor( mainContext: IMainContext, @@ -39,11 +69,9 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape this._proxy.$getTools().then(tools => { for (const tool of tools) { - this._allTools.set(tool.id, revive(tool)); + this._allTools.set(tool.id, new Tool(revive(tool))); } }); - - this._toolInputProcessors.set(EditToolData.id, new EditToolInputProcessor()); } async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { @@ -71,37 +99,51 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape } // Making the round trip here because not all tools were necessarily registered in this EH - const processedInput = this._toolInputProcessors.get(toolId)?.processInput(options.input) ?? options.input; const result = await this._proxy.$invokeTool({ toolId, callId, - parameters: processedInput, + parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined, }, token); - return typeConvert.LanguageModelToolResult.to(revive(result)); + + const dto: Dto = result instanceof SerializableObjectWithBuffers ? result.value : result; + return typeConvert.LanguageModelToolResult2.to(revive(dto)); } finally { this._tokenCountFuncs.delete(callId); } } $onDidChangeTools(tools: IToolDataDto[]): void { - this._allTools.clear(); + + const oldTools = new Set(this._registeredTools.keys()); + for (const tool of tools) { - this._allTools.set(tool.id, tool); + oldTools.delete(tool.id); + const existing = this._allTools.get(tool.id); + if (existing) { + existing.update(tool); + } else { + this._allTools.set(tool.id, new Tool(revive(tool))); + } + } + + for (const id of oldTools) { + this._allTools.delete(id); } } getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] { return Array.from(this._allTools.values()) - .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) + .map(tool => tool.apiObject) .filter(tool => { switch (tool.name) { case InternalEditToolId: case ExtensionEditToolId: case InternalFetchWebPageToolId: + case SearchExtensionsToolId: return isProposedApiEnabled(extension, 'chatParticipantPrivate'); default: return true; @@ -109,7 +151,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }); } - async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise> { + async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise | SerializableObjectWithBuffers>> { const item = this._registeredTools.get(dto.toolId); if (!item) { throw new Error(`Unknown tool ${dto.toolId}`); @@ -141,12 +183,26 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } - const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); + let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined; + if (isProposedApiEnabled(item.extension, 'toolProgress')) { + progress = { + report: value => { + this._proxy.$acceptToolProgress(dto.callId, { + message: typeConvert.MarkdownString.fromStrict(value.message), + increment: value.increment, + total: 100, + }); + } + }; + } + + // todo: 'any' cast because TS can't handle the overloads + const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token); if (!extensionResult) { throw new CancellationError(); } - return typeConvert.LanguageModelToolResult.from(extensionResult, item.extension); + return typeConvert.LanguageModelToolResult2.from(extensionResult, item.extension); } private async getModel(modelId: string, extension: IExtensionDescription): Promise { diff --git a/code/src/vs/workbench/api/common/extHostLanguageModels.ts b/code/src/vs/workbench/api/common/extHostLanguageModels.ts index 65802545889..63a2e81cd41 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -16,7 +16,7 @@ import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IE import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { Progress } from '../../../platform/progress/common/progress.js'; -import { IChatMessage, IChatResponseFragment, IChatResponsePart, ILanguageModelChatMetadata } from '../../contrib/chat/common/languageModels.js'; +import { ChatImageMimeType, IChatMessage, IChatResponseFragment, IChatResponsePart, ILanguageModelChatMetadata } from '../../contrib/chat/common/languageModels.js'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/common/authentication.js'; import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from './extHost.protocol.js'; @@ -25,6 +25,8 @@ import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; +import { VSBuffer } from '../../../base/common/buffer.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../contrib/chat/common/modelPicker/modelPickerWidget.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -101,9 +103,11 @@ class LanguageModelResponse { this._responseStreams.set(fragment.index, res); } - let out: vscode.LanguageModelTextPart | vscode.LanguageModelToolCallPart; + let out: vscode.LanguageModelTextPart | vscode.LanguageModelDataPart | vscode.LanguageModelToolCallPart; if (fragment.part.type === 'text') { out = new extHostTypes.LanguageModelTextPart(fragment.part.value); + } else if (fragment.part.type === 'data') { + out = new extHostTypes.LanguageModelTextPart(''); } else { out = new extHostTypes.LanguageModelToolCallPart(fragment.part.toolCallId, fragment.part.name, fragment.part.parameters); } @@ -173,6 +177,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), name: metadata.name ?? '', family: metadata.family ?? '', + cost: metadata.cost, + description: metadata.description, version: metadata.version, maxInputTokens: metadata.maxInputTokens, maxOutputTokens: metadata.maxOutputTokens, @@ -180,6 +186,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { targetExtensions: metadata.extensions, isDefault: metadata.isDefault, isUserSelectable: metadata.isUserSelectable, + modelPickerCategory: metadata.category ?? DEFAULT_MODEL_PICKER_CATEGORY, capabilities: metadata.capabilities, }); @@ -210,6 +217,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { part = { type: 'tool_use', name: fragment.part.name, parameters: fragment.part.input, toolCallId: fragment.part.callId }; } else if (fragment.part instanceof extHostTypes.LanguageModelTextPart) { part = { type: 'text', value: fragment.part.value }; + } else if (fragment.part instanceof extHostTypes.LanguageModelDataPart) { + part = { type: 'data', value: { mimeType: fragment.part.mimeType as ChatImageMimeType, data: VSBuffer.wrap(fragment.part.data) } }; } if (!part) { @@ -223,24 +232,13 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { let value: any; try { - if (data.provider.provideLanguageModelResponse2) { - value = data.provider.provideLanguageModelResponse2( - messages.value.map(typeConvert.LanguageModelChatMessage2.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - ); - - } else { - value = data.provider.provideLanguageModelResponse( - messages.value.map(typeConvert.LanguageModelChatMessage2.to), - options, - ExtensionIdentifier.toKey(from), - progress, - token - ); - } + value = data.provider.provideLanguageModelResponse( + messages.value.map(typeConvert.LanguageModelChatMessage2.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + ); } catch (err) { // synchronously failed @@ -566,7 +564,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { }; } - fileIsIgnored(extension: IExtensionDescription, uri: vscode.Uri, token: vscode.CancellationToken): Promise { + fileIsIgnored(extension: IExtensionDescription, uri: vscode.Uri, token: vscode.CancellationToken = CancellationToken.None): Promise { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); return this._proxy.$fileIsIgnored(uri, token); diff --git a/code/src/vs/workbench/api/common/extHostMcp.ts b/code/src/vs/workbench/api/common/extHostMcp.ts index ed7e5023e71..7442f2d98ec 100644 --- a/code/src/vs/workbench/api/common/extHostMcp.ts +++ b/code/src/vs/workbench/api/common/extHostMcp.ts @@ -3,35 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as ES from '@c4312/eventsource-umd'; import * as vscode from 'vscode'; -import { importAMDNodeModule } from '../../../amdX.js'; -import { DeferredPromise, Sequencer } from '../../../base/common/async.js'; -import { CancellationToken } from '../../../base/common/cancellation.js'; -import { Lazy } from '../../../base/common/lazy.js'; +import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { SSEParser } from '../../../base/common/sseParser.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; +import { LogLevel } from '../../../platform/log/common/log.js'; import { StorageScope } from '../../../platform/storage/common/storage.js'; -import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportSSE, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { LogLevel } from '../../../platform/log/common/log.js'; +import * as Convert from './extHostTypeConverters.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); export interface IExtHostMpcService extends ExtHostMcpShape { - registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable; + registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable; } export class ExtHostMcpService extends Disposable implements IExtHostMpcService { protected _proxy: MainThreadMcpShape; private readonly _initialProviderPromises = new Set>(); - private readonly _sseEventSources = this._register(new DisposableMap()); - private readonly _eventSource = new Lazy(async () => { - const es = await importAMDNodeModule('@c4312/eventsource-umd', 'dist/index.umd.js'); - return es.EventSource; - }); + private readonly _sseEventSources = this._register(new DisposableMap()); + private readonly _unresolvedMcpServers = new Map(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -45,8 +44,8 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService } protected _startMcp(id: number, launch: McpServerLaunch): void { - if (launch.type === McpServerTransportType.SSE) { - this._sseEventSources.set(id, new McpSSEHandle(this._eventSource.value, id, launch, this._proxy)); + if (launch.type === McpServerTransportType.HTTP) { + this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy)); return; } @@ -68,8 +67,26 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService await Promise.all(this._initialProviderPromises); } - /** {@link vscode.lm.registerMcpConfigurationProvider} */ - public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { + async $resolveMcpLaunch(collectionId: string, label: string): Promise { + const rec = this._unresolvedMcpServers.get(collectionId); + if (!rec) { + return; + } + + const server = rec.servers.find(s => s.label === label); + if (!server) { + return; + } + if (!rec.provider.resolveMcpServerDefinition) { + return Convert.McpServerDefinition.from(server); + } + + const resolved = await rec.provider.resolveMcpServerDefinition(server, CancellationToken.None); + return resolved ? Convert.McpServerDefinition.from(resolved) : undefined; + } + + /** {@link vscode.lm.registerMcpServerDefinitionProvider} */ + public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable { const store = new DisposableStore(); const metadata = extension.contributes?.modelContextServerCollections?.find(m => m.id === id); @@ -81,37 +98,29 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService id: extensionPrefixedIdentifier(extension.identifier, id), isTrustedByDefault: true, label: metadata?.label ?? extension.displayName ?? extension.name, - scope: StorageScope.WORKSPACE + scope: StorageScope.WORKSPACE, + canResolveLaunch: typeof provider.resolveMcpServerDefinition === 'function', + extensionId: extension.identifier.value, }; const update = async () => { - const list = await provider.provideMcpServerDefinitions(CancellationToken.None); + this._unresolvedMcpServers.set(mcp.id, { servers: list ?? [], provider }); - function isSSEConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpSSEServerDefinition { - return !!(candidate as vscode.McpSSEServerDefinition).uri; - } - - const servers: McpServerDefinition[] = []; - + const servers: McpServerDefinition.Serialized[] = []; for (const item of list ?? []) { + let id = ExtensionIdentifier.toKey(extension.identifier) + '/' + item.label; + if (servers.some(s => s.id === id)) { + let i = 2; + while (servers.some(s => s.id === id + i)) { i++; } + id = id + i; + } + servers.push({ - id: ExtensionIdentifier.toKey(extension.identifier), + id, label: item.label, - launch: isSSEConfig(item) - ? { - type: McpServerTransportType.SSE, - uri: item.uri, - headers: item.headers, - } - : { - type: McpServerTransportType.Stdio, - cwd: item.cwd, - args: item.args, - command: item.command, - env: item.env, - envFile: undefined, - } + cacheNonce: item.version, + launch: Convert.McpServerDefinition.from(item) }); } @@ -119,11 +128,16 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService }; store.add(toDisposable(() => { + this._unresolvedMcpServers.delete(mcp.id); this._proxy.$deleteMcpCollection(mcp.id); })); - if (provider.onDidChange) { - store.add(provider.onDidChange(update)); + if (provider.onDidChangeServerDefinitions) { + store.add(provider.onDidChangeServerDefinitions(update)); + } + // todo@connor4312: proposed API back-compat + if ((provider as any).onDidChange) { + store.add((provider as any).onDidChange(update)); } const promise = new Promise(resolve => { @@ -139,96 +153,313 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService } } -class McpSSEHandle extends Disposable { +const enum HttpMode { + Unknown, + Http, + SSE, +} + +type HttpModeT = + | { value: HttpMode.Unknown } + | { value: HttpMode.Http; sessionId: string | undefined } + | { value: HttpMode.SSE; endpoint: string }; + +/** + * Implementation of both MCP HTTP Streaming as well as legacy SSE. + * + * The first request will POST to the endpoint, assuming HTTP streaming. If the + * server is legacy SSE, it should return some 4xx status in that case, + * and we'll automatically fall back to SSE and res + */ +class McpHTTPHandle extends Disposable { private readonly _requestSequencer = new Sequencer(); - private readonly _postEndpoint = new DeferredPromise(); + private readonly _postEndpoint = new DeferredPromise<{ url: string; transport: McpServerTransportHTTP }>(); + private _mode: HttpModeT = { value: HttpMode.Unknown }; + private readonly _cts = new CancellationTokenSource(); + private readonly _abortCtrl = new AbortController(); + constructor( - eventSourceCtor: Promise, private readonly _id: number, - launch: McpServerTransportSSE, + private readonly _launch: McpServerTransportHTTP, private readonly _proxy: MainThreadMcpShape ) { super(); - eventSourceCtor.then(EventSourceCtor => this._attach(EventSourceCtor, launch)); + + this._register(toDisposable(() => { + this._abortCtrl.abort(); + this._cts.dispose(true); + })); + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running }); } - private _attach(EventSourceCtor: typeof ES.EventSource, launch: McpServerTransportSSE) { - if (this._store.isDisposed) { - return; + async send(message: string) { + try { + await this._requestSequencer.queue(() => { + if (this._mode.value === HttpMode.SSE) { + return this._sendLegacySSE(this._mode.endpoint, message); + } else { + return this._sendStreamableHttp(message, this._mode.value === HttpMode.Http ? this._mode.sessionId : undefined); + } + }); + } catch (err) { + const msg = `Error sending message to ${this._launch.uri}: ${String(err)}`; + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: msg }); } + } - const eventSource = new EventSourceCtor(launch.uri.toString(), { - // recommended way to do things https://github.com/EventSource/eventsource?tab=readme-ov-file#setting-http-request-headers - fetch: (input, init) => - fetch(input, { - ...init, - headers: { - ...Object.fromEntries(launch.headers), - ...init?.headers, - }, - }).then(async res => { - // we get more details on failure at this point, so handle it explicitly: - if (res.status >= 300) { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${launch.uri}: ${await this._getErrText(res)}` }); - eventSource.close(); - } - return res; - }, err => { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${launch.uri}: ${String(err)}` }); + /** + * Sends a streamable-HTTP request. + * 1. Posts to the endpoint + * 2. Updates internal state as needed. Falls back to SSE if appropriate. + * 3. If the response body is empty, JSON, or a JSON stream, handle it appropriately. + */ + private async _sendStreamableHttp(message: string, sessionId: string | undefined) { + const asBytes = new TextEncoder().encode(message); + const headers: Record = { + ...Object.fromEntries(this._launch.headers), + 'Content-Type': 'application/json', + 'Content-Length': String(asBytes.length), + Accept: 'text/event-stream, application/json', + }; + if (sessionId) { + headers['Mcp-Session-Id'] = sessionId; + } - eventSource.close(); - return Promise.reject(err); - }) + const res = await fetch(this._launch.uri.toString(true), { + method: 'POST', + signal: this._abortCtrl.signal, + headers, + body: asBytes, }); - this._register(toDisposable(() => eventSource.close())); + const wasUnknown = this._mode.value === HttpMode.Unknown; - // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L52 - eventSource.addEventListener('endpoint', e => { - this._postEndpoint.complete(new URL(e.data, launch.uri.toString()).toString()); - }); + // Mcp-Session-Id is the strongest signal that we're in streamable HTTP mode + const nextSessionId = res.headers.get('Mcp-Session-Id'); + if (nextSessionId) { + this._mode = { value: HttpMode.Http, sessionId: nextSessionId }; + } - // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L133 - eventSource.addEventListener('message', e => { - this._proxy.$onDidReceiveMessage(this._id, e.data); - }); + if (this._mode.value === HttpMode.Unknown && res.status >= 400 && res.status < 500) { + this._log(LogLevel.Info, `${res.status} status sending message to ${this._launch.uri}, will attempt to fall back to legacy SSE`); + const endpoint = await this._attachSSE(); + if (endpoint) { + this._mode = { value: HttpMode.SSE, endpoint }; + await this._sendLegacySSE(endpoint, message); + } + return; + } - eventSource.addEventListener('open', () => { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running }); - }); + if (res.status >= 300) { + // "When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached" + // Though this says only 404, some servers send 400s as well, including their example + // https://github.com/modelcontextprotocol/typescript-sdk/issues/389 + const retryWithSessionId = this._mode.value === HttpMode.Http && !!this._mode.sessionId; - eventSource.addEventListener('error', (err) => { - this._postEndpoint.cancel(); this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, - message: `Error connecting to ${launch.uri}: ${err.code || 0} ${err.message || JSON.stringify(err)}`, + message: `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}` + retryWithSessionId ? `; will retry with new session ID` : '', + shouldRetry: retryWithSessionId, }); - eventSource.close(); - }); + return; + } + + if (this._mode.value === HttpMode.Unknown) { + this._mode = { value: HttpMode.Http, sessionId: undefined }; + } + if (wasUnknown) { + this._attachStreamableBackchannel(); + } + + // Not awaited, we don't need to block the sequencer while we read the response + this._handleSuccessfulStreamableHttp(res); } - async send(message: string) { - // only the sending of the request needs to be sequenced - try { - const res = await this._requestSequencer.queue(async () => { - const endpoint = await this._postEndpoint.p; - const asBytes = new TextEncoder().encode(message); - - return fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': String(asBytes.length), - }, - body: asBytes, + private async _handleSuccessfulStreamableHttp(res: Response) { + if (res.status === 202) { + return; // no body + } + + switch (res.headers.get('Content-Type')?.toLowerCase()) { + case 'text/event-stream': { + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } + }); + + try { + await this._doSSE(parser, res); + } catch (err) { + this._log(LogLevel.Warning, `Error reading SSE stream: ${String(err)}`); + } + break; + } + case 'application/json': + this._proxy.$onDidReceiveMessage(this._id, await res.text()); + break; + default: { + const responseBody = await res.text(); + if (isJSON(responseBody)) { // try to read as JSON even if the server didn't set the content type + this._proxy.$onDidReceiveMessage(this._id, responseBody); + } else { + this._log(LogLevel.Warning, `Unexpected ${res.status} response for request: ${responseBody}`); + } + } + } + } + + /** + * Attaches the SSE backchannel that streamable HTTP servers can use + * for async notifications. This is a "MAY" support, so if the server gives + * us a 4xx code, we'll stop trying to connect.. + */ + private async _attachStreamableBackchannel() { + let lastEventId: string | undefined; + for (let retry = 0; !this._store.isDisposed; retry++) { + await timeout(Math.min(retry * 1000, 30_000), this._cts.token); + + let res: Response; + try { + const headers: Record = { + ...Object.fromEntries(this._launch.headers), + 'Accept': 'text/event-stream', + }; + + if (this._mode.value === HttpMode.Http && this._mode.sessionId !== undefined) { + headers['Mcp-Session-Id'] = this._mode.sessionId; + } + if (lastEventId) { + headers['Last-Event-ID'] = lastEventId; + } + + res = await fetch(this._launch.uri.toString(true), { + method: 'GET', + signal: this._abortCtrl.signal, + headers, }); + } catch (e) { + this._log(LogLevel.Info, `Error connecting to ${this._launch.uri} for async notifications, will retry`); + continue; + } + + if (res.status >= 400) { + this._log(LogLevel.Debug, `${res.status} status connecting to ${this._launch.uri} for async notifications; they will be disabled: ${await this._getErrText(res)}`); + return; + } + + retry = 0; + + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } + if (event.id) { + lastEventId = event.id; + } }); + try { + await this._doSSE(parser, res); + } catch (e) { + this._log(LogLevel.Info, `Error reading from async stream, we will reconnect: ${e}`); + } + } + } + + /** + * Starts a legacy SSE attachment, where the SSE response is the session lifetime. + * Unlike `_attachStreamableBackchannel`, this fails the server if it disconnects. + */ + private async _attachSSE(): Promise { + const postEndpoint = new DeferredPromise(); + + let res: Response; + try { + res = await fetch(this._launch.uri.toString(true), { + method: 'GET', + signal: this._abortCtrl.signal, + headers: { + ...Object.fromEntries(this._launch.headers), + 'Accept': 'text/event-stream', + }, + }); if (res.status >= 300) { - this._proxy.$onDidPublishLog(this._id, LogLevel.Warning, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`); + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${this._launch.uri} as SSE: ${await this._getErrText(res)}` }); + return; } - } catch (err) { - // ignored + } catch (e) { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${this._launch.uri} as SSE: ${e}` }); + return; + } + + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } else if (event.type === 'endpoint') { + postEndpoint.complete(new URL(event.data, this._launch.uri.toString(true)).toString()); + } + }); + + this._register(toDisposable(() => postEndpoint.cancel())); + this._doSSE(parser, res).catch(err => { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error reading SSE stream: ${String(err)}` }); + }); + + return postEndpoint.p; + } + + /** + * Sends a legacy SSE message to the server. The response is always empty and + * is otherwise received in {@link _attachSSE}'s loop. + */ + private async _sendLegacySSE(url: string, message: string) { + const asBytes = new TextEncoder().encode(message); + const res = await fetch(url, { + method: 'POST', + signal: this._abortCtrl.signal, + headers: { + ...Object.fromEntries(this._launch.headers), + 'Content-Type': 'application/json', + 'Content-Length': String(asBytes.length), + }, + body: asBytes, + }); + + if (res.status >= 300) { + this._log(LogLevel.Warning, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`); + } + } + + /** Generic handle to pipe a response into an SSE parser. */ + private async _doSSE(parser: SSEParser, res: Response) { + if (!res.body) { + return; + } + + const reader = res.body.getReader(); + let chunk: ReadableStreamReadResult; + do { + try { + chunk = await raceCancellationError(reader.read(), this._cts.token); + } catch (err) { + reader.cancel(); + if (this._store.isDisposed) { + return; + } else { + throw err; + } + } + + if (chunk.value) { + parser.feed(chunk.value); + } + } while (!chunk.done); + } + + private _log(level: LogLevel, message: string) { + if (!this._store.isDisposed) { + this._proxy.$onDidPublishLog(this._id, level, message); } } @@ -240,3 +471,12 @@ class McpSSEHandle extends Disposable { } } } + +function isJSON(str: string): boolean { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +} diff --git a/code/src/vs/workbench/api/common/extHostNotebookKernels.ts b/code/src/vs/workbench/api/common/extHostNotebookKernels.ts index 9c923a42745..1994bc803b5 100644 --- a/code/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/code/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -84,6 +84,9 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { if (notebookEditorId === undefined) { throw new Error(`Cannot invoke 'notebook.selectKernel' for unrecognized notebook editor ${v.notebookEditor.notebook.uri.toString()}`); } + if ('skipIfAlreadySelected' in v) { + return { notebookEditorId, skipIfAlreadySelected: v.skipIfAlreadySelected }; + } return { notebookEditorId }; } return v; diff --git a/code/src/vs/workbench/api/common/extHostQuickDiff.ts b/code/src/vs/workbench/api/common/extHostQuickDiff.ts index 2b04fcd6222..b17b67c6a3d 100644 --- a/code/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/code/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -10,6 +10,7 @@ import { ExtHostQuickDiffShape, IMainContext, MainContext, MainThreadQuickDiffSh import { asPromise } from '../../../base/common/async.js'; import { DocumentSelector } from './extHostTypeConverters.js'; import { IURITransformer } from '../../../base/common/uriIpc.js'; +import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; export class ExtHostQuickDiff implements ExtHostQuickDiffShape { private static handlePool: number = 0; @@ -36,10 +37,12 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { .then(r => r || null); } - registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { + registerQuickDiffProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, id: string, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); - this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri, quickDiffProvider.visible ?? true); + + const extensionId = ExtensionIdentifier.toKey(extension.identifier); + this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), `${extensionId}.${id}`, label, rootUri); return { dispose: () => { this.proxy.$unregisterQuickDiffProvider(handle); diff --git a/code/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/code/src/vs/workbench/api/common/extHostRequireInterceptor.ts index f01e80dd53e..e106a29e575 100644 --- a/code/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/code/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/code/src/vs/workbench/api/common/extHostSCM.ts b/code/src/vs/workbench/api/common/extHostSCM.ts index 8ec74753cd6..487f91504eb 100644 --- a/code/src/vs/workbench/api/common/extHostSCM.ts +++ b/code/src/vs/workbench/api/common/extHostSCM.ts @@ -591,6 +591,21 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel }); } + private _secondaryQuickDiffProvider: vscode.QuickDiffProvider | undefined = undefined; + + get secondaryQuickDiffProvider(): vscode.QuickDiffProvider | undefined { + checkProposedApiEnabled(this._extension, 'quickDiffProvider'); + return this._secondaryQuickDiffProvider; + } + + set secondaryQuickDiffProvider(secondaryQuickDiffProvider: vscode.QuickDiffProvider | undefined) { + checkProposedApiEnabled(this._extension, 'quickDiffProvider'); + + this._secondaryQuickDiffProvider = secondaryQuickDiffProvider; + const secondaryQuickDiffLabel = secondaryQuickDiffProvider?.label; + this.#proxy.$updateSourceControl(this.handle, { hasSecondaryQuickDiffProvider: !!secondaryQuickDiffProvider, secondaryQuickDiffLabel }); + } + private _historyProvider: vscode.SourceControlHistoryProvider | undefined; private readonly _historyProviderDisposable = new MutableDisposable(); @@ -944,6 +959,20 @@ export class ExtHostSCM implements ExtHostSCMShape { .then(r => r || null); } + $provideSecondaryOriginalResource(sourceControlHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise { + const uri = URI.revive(uriComponents); + this.logService.trace('ExtHostSCM#$provideSecondaryOriginalResource', sourceControlHandle, uri.toString()); + + const sourceControl = this._sourceControls.get(sourceControlHandle); + + if (!sourceControl || !sourceControl.secondaryQuickDiffProvider || !sourceControl.secondaryQuickDiffProvider.provideOriginalResource) { + return Promise.resolve(null); + } + + return asPromise(() => sourceControl.secondaryQuickDiffProvider!.provideOriginalResource!(uri, token)) + .then(r => r || null); + } + $onInputBoxValueChange(sourceControlHandle: number, value: string): Promise { this.logService.trace('ExtHostSCM#$onInputBoxValueChange', sourceControlHandle); diff --git a/code/src/vs/workbench/api/common/extHostSearch.ts b/code/src/vs/workbench/api/common/extHostSearch.ts index fb92adb1fc9..92ba9241bf8 100644 --- a/code/src/vs/workbench/api/common/extHostSearch.ts +++ b/code/src/vs/workbench/api/common/extHostSearch.ts @@ -176,7 +176,7 @@ export class ExtHostSearch implements IExtHostSearch { const query = reviveQuery(rawQuery); const engine = this.createAITextSearchManager(query, provider); - return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); + return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token, result => this._proxy.$handleKeywordResult(handle, session, result)); } $enableExtensionHostSearch(): void { } diff --git a/code/src/vs/workbench/api/common/extHostTypeConverters.ts b/code/src/vs/workbench/api/common/extHostTypeConverters.ts index 75a62af117c..e6fd3913d5f 100644 --- a/code/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/code/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -34,18 +34,18 @@ import * as languageSelector from '../../../editor/common/languageSelector.js'; import * as languages from '../../../editor/common/languages.js'; import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js'; import { ITextEditorOptions } from '../../../platform/editor/common/editor.js'; -import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; -import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; -import { IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; +import { IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; import * as notebooks from '../../contrib/notebook/common/notebookCommon.js'; import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js'; @@ -55,14 +55,17 @@ import { TestId } from '../../contrib/testing/common/testId.js'; import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestRunProfileReference, ITestTag, TestMessageType, TestResultItem, TestRunProfileBitset, denamespaceTestTag, namespaceTestTag } from '../../contrib/testing/common/testTypes.js'; import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.js'; -import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import * as extHostProtocol from './extHost.protocol.js'; import { CommandsConverter } from './extHostCommands.js'; import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; -import { LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; +import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { ILogService } from '../../../platform/log/common/log.js'; export namespace Command { @@ -2327,10 +2330,9 @@ export namespace LanguageModelChatMessage { } }); return new types.LanguageModelToolResultPart(c.toolCallId, content, c.isError); - } else if (c.type === 'image_url') { - // No image support for LanguageModelChatMessage + } else if (c.type === 'image_url' || c.type === 'extra_data') { + // Non-stable types return undefined; - } else { return new types.LanguageModelToolCallPart(c.toolCallId, c.name, c.parameters); } @@ -2413,21 +2415,20 @@ export namespace LanguageModelChatMessage2 { if (c.type === 'text') { return new LanguageModelTextPart(c.value); } else if (c.type === 'tool_result') { - const content: (LanguageModelTextPart | LanguageModelPromptTsxPart)[] = c.value.map(part => { + const content: (LanguageModelTextPart | LanguageModelPromptTsxPart | LanguageModelDataPart)[] = c.value.map(part => { if (part.type === 'text') { return new types.LanguageModelTextPart(part.value); + } else if (part.type === 'data') { + return new types.LanguageModelDataPart(part.value.data.buffer, part.value.mimeType); } else { return new types.LanguageModelPromptTsxPart(part.value); } }); - return new types.LanguageModelToolResultPart(c.toolCallId, content, c.isError); + return new types.LanguageModelToolResultPart2(c.toolCallId, content, c.isError); } else if (c.type === 'image_url') { - const value: vscode.ChatImagePart = { - mimeType: c.value.mimeType, - data: c.value.data.buffer, - }; - - return new types.LanguageModelDataPart(value); + return new types.LanguageModelDataPart(c.value.data.buffer, c.value.mimeType); + } else if (c.type === 'extra_data') { + return new types.LanguageModelExtraDataPart(c.kind, c.data); } else { return new types.LanguageModelToolCallPart(c.toolCallId, c.name, c.parameters); } @@ -2448,7 +2449,7 @@ export namespace LanguageModelChatMessage2 { } const content = messageContent.map((c): chatProvider.IChatMessagePart => { - if (c instanceof types.LanguageModelToolResultPart) { + if ((c instanceof types.LanguageModelToolResultPart2) || (c instanceof types.LanguageModelToolResultPart)) { return { type: 'tool_result', toolCallId: c.callId, @@ -2463,6 +2464,14 @@ export namespace LanguageModelChatMessage2 { type: 'prompt_tsx', value: part.value, } satisfies IChatResponsePromptTsxPart; + } else if (part instanceof types.LanguageModelDataPart) { + return { + type: 'data', + value: { + mimeType: part.mimeType as chatProvider.ChatImageMimeType, + data: VSBuffer.wrap(part.data) + } + } satisfies IChatResponseDataPart; } else { // Strip unknown parts return undefined; @@ -2472,8 +2481,8 @@ export namespace LanguageModelChatMessage2 { }; } else if (c instanceof types.LanguageModelDataPart) { const value: chatProvider.IChatImageURLPart = { - mimeType: c.value.mimeType, - data: VSBuffer.wrap(c.value.data), + mimeType: c.mimeType as chatProvider.ChatImageMimeType, + data: VSBuffer.wrap(c.data), }; return { @@ -2492,9 +2501,15 @@ export namespace LanguageModelChatMessage2 { type: 'text', value: c.value }; + } else if (c instanceof types.LanguageModelExtraDataPart) { + return { + type: 'extra_data', + kind: c.kind, + data: c.data + } satisfies chatProvider.IChatMessagePart; } else { if (typeof c !== 'string') { - throw new Error('Unexpected chat message content type'); + throw new Error('Unexpected chat message content type llm 2'); } return { @@ -2655,6 +2670,15 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatResponseExtensionsPart { + export function from(part: vscode.ChatResponseExtensionsPart): Dto { + return { + kind: 'extensions', + extensions: part.extensions + }; + } +} + export namespace ChatResponseMovePart { export function from(part: vscode.ChatResponseMovePart): Dto { return { @@ -2814,7 +2838,7 @@ export namespace ChatResponseCodeCitationPart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart | vscode.ChatResponseExtensionsPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2843,6 +2867,8 @@ export namespace ChatResponsePart { return ChatResponseCodeCitationPart.from(part); } else if (part instanceof types.ChatResponseMovePart) { return ChatResponseMovePart.from(part); + } else if (part instanceof types.ChatResponseExtensionsPart) { + return ChatResponseExtensionsPart.from(part); } return { @@ -2878,33 +2904,49 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined): vscode.ChatRequest { - const toolReferences = request.variables.variables.filter(v => v.isTool); - const variableReferences = request.variables.variables.filter(v => !v.isTool); - const requestWithoutId = { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], toolSelection: vscode.ChatRequestToolSelection | undefined, tools: Map, extension: IRelaxedExtensionDescription, logService: ILogService): vscode.ChatRequest { + const toolReferences = request.variables.variables.filter(v => v.kind === 'tool'); + const variableReferences = request.variables.variables.filter(v => v.kind !== 'tool'); + const requestWithAllProps: vscode.ChatRequest = { + id: request.requestId, prompt: request.message, command: request.command, attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, isParticipantDetected: request.isParticipantDetected ?? false, - references: variableReferences.map(v => ChatPromptReference.to(v, diagnostics)), + references: variableReferences + .map(v => ChatPromptReference.to(v, diagnostics, logService)) + .filter(isDefined), toolReferences: toolReferences.map(ChatLanguageModelToolReference.to), location: ChatLocation.to(request.location), acceptedConfirmationData: request.acceptedConfirmationData, rejectedConfirmationData: request.rejectedConfirmationData, location2, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, + toolSelection, tools, - model + model, + editedFileEvents: request.editedFileEvents, }; - if (request.requestId) { - return { - ...requestWithoutId, - id: request.requestId - }; + + if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + delete (requestWithAllProps as any).id; + delete (requestWithAllProps as any).attempt; + delete (requestWithAllProps as any).enableCommandDetection; + delete (requestWithAllProps as any).isParticipantDetected; + delete (requestWithAllProps as any).location; + delete (requestWithAllProps as any).location2; + delete (requestWithAllProps as any).editedFileEvents; } - // This cast is done to allow sending the stabl version of ChatRequest which does not have an id property - return requestWithoutId as unknown as vscode.ChatRequest; + + if (!isProposedApiEnabled(extension, 'chatParticipantAdditions')) { + delete requestWithAllProps.acceptedConfirmationData; + delete requestWithAllProps.rejectedConfirmationData; + delete (requestWithAllProps as any).tools; + } + + + return requestWithAllProps; } } @@ -2924,7 +2966,6 @@ export namespace ChatLocation { case ChatAgentLocation.Terminal: return types.ChatLocation.Terminal; case ChatAgentLocation.Panel: return types.ChatLocation.Panel; case ChatAgentLocation.Editor: return types.ChatLocation.Editor; - case ChatAgentLocation.EditingSession: return types.ChatLocation.EditingSession; } } @@ -2934,23 +2975,30 @@ export namespace ChatLocation { case types.ChatLocation.Terminal: return ChatAgentLocation.Terminal; case types.ChatLocation.Panel: return ChatAgentLocation.Panel; case types.ChatLocation.Editor: return ChatAgentLocation.Editor; - case types.ChatLocation.EditingSession: return ChatAgentLocation.EditingSession; } } } export namespace ChatPromptReference { - export function to(variable: IChatRequestVariableEntry, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatPromptReference { + export function to(variable: IChatRequestVariableEntry, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], logService: ILogService): vscode.ChatPromptReference | undefined { let value: vscode.ChatPromptReference['value'] = variable.value; if (!value) { - throw new Error('Invalid value reference'); + let varStr: string; + try { + varStr = JSON.stringify(variable); + } catch { + varStr = `kind=${variable.kind}, id=${variable.id}, name=${variable.name}`; + } + + logService.error(`[ChatPromptReference] Ignoring invalid reference in variable: ${varStr}`); + return undefined; } if (isUriComponents(value)) { value = URI.revive(value); } else if (value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri)) { value = Location.to(revive(value)); - } else if (variable.isImage) { + } else if (isImageVariableEntry(variable)) { const ref = variable.references?.[0]?.reference; value = new types.ChatReferenceBinaryData( variable.mimeType ?? 'image/png', @@ -3159,6 +3207,26 @@ export namespace PartialAcceptTriggerKind { } } +export namespace InlineCompletionEndOfLifeReason { + export function to(reason: languages.InlineCompletionEndOfLifeReason, convertFn: (item: T) => vscode.InlineCompletionItem | undefined): vscode.InlineCompletionEndOfLifeReason { + if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Ignored) { + const supersededBy = reason.supersededBy ? convertFn(reason.supersededBy) : undefined; + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Ignored, + supersededBy: supersededBy, + userTypingDisagreed: reason.userTypingDisagreed, + }; + } else if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Accepted) { + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Accepted, + }; + } + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Rejected, + }; + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { @@ -3222,8 +3290,112 @@ export namespace LanguageModelToolResult { } } +export namespace LanguageModelToolResult2 { + export function to(result: IToolResult): vscode.LanguageModelToolResult2 { + return new types.LanguageModelToolResult2(result.content.map(item => { + if (item.kind === 'text') { + return new types.LanguageModelTextPart(item.value); + } else if (item.kind === 'data') { + const mimeType = Object.values(types.ChatImageMimeType).includes(item.value.mimeType as types.ChatImageMimeType) ? item.value.mimeType as types.ChatImageMimeType : undefined; + if (!mimeType) { + throw new Error('Invalid MIME type'); + } + return new types.LanguageModelDataPart(item.value.data.buffer, mimeType); + } else { + return new types.LanguageModelPromptTsxPart(item.value); + } + })); + } + + export function from(result: vscode.ExtendedLanguageModelToolResult, extension: IExtensionDescription): Dto | SerializableObjectWithBuffers> { + if (result.toolResultMessage) { + checkProposedApiEnabled(extension, 'chatParticipantPrivate'); + } + + let hasBuffers = false; + const dto: Dto = { + content: result.content.map(item => { + if (item instanceof types.LanguageModelTextPart) { + return { + kind: 'text', + value: item.value + }; + } else if (item instanceof types.LanguageModelPromptTsxPart) { + return { + kind: 'promptTsx', + value: item.value, + }; + } else if (item instanceof types.LanguageModelDataPart) { + hasBuffers = true; + return { + kind: 'data', + value: { + mimeType: item.mimeType, + data: VSBuffer.wrap(item.data) + } + }; + } else { + throw new Error('Unknown LanguageModelToolResult part type'); + } + }), + toolResultMessage: MarkdownString.fromStrict(result.toolResultMessage), + toolResultDetails: result.toolResultDetails?.map(detail => URI.isUri(detail) ? detail : Location.from(detail as vscode.Location)), + }; + + return hasBuffers ? new SerializableObjectWithBuffers(dto) : dto; + } +} + export namespace IconPath { export function fromThemeIcon(iconPath: vscode.ThemeIcon): languages.IconPath { return iconPath; } } + +export namespace AiSettingsSearch { + export function fromSettingsSearchResult(result: vscode.SettingsSearchResult): AiSettingsSearchResult { + return { + query: result.query, + kind: fromSettingsSearchResultKind(result.kind), + settings: result.settings + }; + } + + function fromSettingsSearchResultKind(kind: number): AiSettingsSearchResultKind { + switch (kind) { + case AiSettingsSearchResultKind.EMBEDDED: + return AiSettingsSearchResultKind.EMBEDDED; + case AiSettingsSearchResultKind.LLM_RANKED: + return AiSettingsSearchResultKind.LLM_RANKED; + case AiSettingsSearchResultKind.CANCELED: + return AiSettingsSearchResultKind.CANCELED; + default: + throw new Error('Unknown AiSettingsSearchResultKind'); + } + } +} + +export namespace McpServerDefinition { + function isHttpConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpHttpServerDefinition { + return !!(candidate as vscode.McpHttpServerDefinition).uri; + } + + export function from(item: vscode.McpServerDefinition): McpServerLaunch.Serialized { + return McpServerLaunch.toSerialized( + isHttpConfig(item) + ? { + type: McpServerTransportType.HTTP, + uri: item.uri, + headers: Object.entries(item.headers), + } + : { + type: McpServerTransportType.Stdio, + cwd: item.cwd, + args: item.args, + command: item.command, + env: item.env, + envFile: undefined, + } + ); + } +} diff --git a/code/src/vs/workbench/api/common/extHostTypes.ts b/code/src/vs/workbench/api/common/extHostTypes.ts index 2a5159f0732..4590e039171 100644 --- a/code/src/vs/workbench/api/common/extHostTypes.ts +++ b/code/src/vs/workbench/api/common/extHostTypes.ts @@ -1866,6 +1866,12 @@ export enum PartialAcceptTriggerKind { Suggest = 3, } +export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2, +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -4519,6 +4525,12 @@ export enum ChatEditingSessionActionOutcome { Saved = 3 } +export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, +} + //#endregion //#region Interactive Editor @@ -4674,6 +4686,13 @@ export class ChatResponseMovePart { } } +export class ChatResponseExtensionsPart { + constructor( + public readonly extensions: string[], + ) { + } +} + export class ChatResponseTextEditPart implements vscode.ChatResponseTextEditPart { uri: vscode.Uri; edits: vscode.TextEdit[]; @@ -4705,13 +4724,14 @@ export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebook } } -export class ChatRequestTurn implements vscode.ChatRequestTurn { +export class ChatRequestTurn implements vscode.ChatRequestTurn2 { constructor( readonly prompt: string, readonly command: string | undefined, readonly references: vscode.ChatPromptReference[], readonly participant: string, - readonly toolReferences: vscode.ChatLanguageModelToolReference[] + readonly toolReferences: vscode.ChatLanguageModelToolReference[], + readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[] ) { } } @@ -4730,7 +4750,6 @@ export enum ChatLocation { Terminal = 2, Notebook = 3, Editor = 4, - EditingSession = 5, } export enum ChatResponseReferencePartStatusKind { @@ -4787,6 +4806,19 @@ export class LanguageModelToolResultPart implements vscode.LanguageModelToolResu } } +export class LanguageModelToolResultPart2 implements vscode.LanguageModelToolResultPart2 { + + callId: string; + content: (LanguageModelTextPart | LanguageModelPromptTsxPart | LanguageModelDataPart | unknown)[]; + isError: boolean; + + constructor(callId: string, content: (LanguageModelTextPart | LanguageModelPromptTsxPart | LanguageModelDataPart | unknown)[], isError?: boolean) { + this.callId = callId; + this.content = content; + this.isError = isError ?? false; + } +} + export class PreparedTerminalToolInvocation { constructor( public readonly command: string, @@ -4829,27 +4861,6 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage return this._content; } - // Temp to avoid breaking changes - set content2(value: (string | LanguageModelToolResultPart | LanguageModelToolCallPart)[] | undefined) { - if (value) { - this.content = value.map(part => { - if (typeof part === 'string') { - return new LanguageModelTextPart(part); - } - return part; - }); - } - } - - get content2(): (string | LanguageModelToolResultPart | LanguageModelToolCallPart)[] | undefined { - return this.content.map(part => { - if (part instanceof LanguageModelTextPart) { - return part.value; - } - return part; - }); - } - name: string | undefined; constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart)[], name?: string) { @@ -4859,22 +4870,21 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage } } - export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessage2 { - static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage2 { + static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string): LanguageModelChatMessage2 { return new LanguageModelChatMessage2(LanguageModelChatMessageRole.User, content, name); } - static Assistant(content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage2 { + static Assistant(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string): LanguageModelChatMessage2 { return new LanguageModelChatMessage2(LanguageModelChatMessageRole.Assistant, content, name); } role: vscode.LanguageModelChatMessageRole; - private _content: (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[] = []; + private _content: (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] = []; - set content(value: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[]) { + set content(value: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[]) { if (typeof value === 'string') { // we changed this and still support setting content with a string property. this keep the API runtime stable // despite the breaking change in the type definition. @@ -4884,12 +4894,12 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } - get content(): (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[] { + get content(): (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] { return this._content; } // Temp to avoid breaking changes - set content2(value: (string | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[] | undefined) { + set content2(value: (string | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[] | undefined) { if (value) { this.content = value.map(part => { if (typeof part === 'string') { @@ -4900,7 +4910,7 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } - get content2(): (string | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[] | undefined { + get content2(): (string | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] | undefined { return this.content.map(part => { if (part instanceof LanguageModelTextPart) { return part.value; @@ -4911,7 +4921,7 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag name: string | undefined; - constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string) { + constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string) { this.role = role; this.content = content; this.name = name; @@ -4948,23 +4958,36 @@ export class LanguageModelTextPart implements vscode.LanguageModelTextPart { } export class LanguageModelDataPart implements vscode.LanguageModelDataPart { - value: vscode.ChatImagePart; + mimeType: string; + data: Uint8Array; - constructor(value: vscode.ChatImagePart) { - this.value = value; + constructor(data: Uint8Array, mimeType: string) { + this.mimeType = mimeType; + this.data = data; + } + + static image(data: Uint8Array, mimeType: ChatImageMimeType): vscode.LanguageModelDataPart { + return new LanguageModelDataPart(data, mimeType as string); + } + + static json(value: object): vscode.LanguageModelDataPart { + const rawStr = JSON.stringify(value, undefined, '\t'); + return new LanguageModelDataPart(VSBuffer.fromString(rawStr).buffer, 'json'); + } + + static text(value: string): vscode.LanguageModelDataPart { + return new LanguageModelDataPart(VSBuffer.fromString(value).buffer, 'text/plain'); } toJSON() { return { $mid: MarshalledId.LanguageModelDataPart, - value: this.value, + mimeType: this.mimeType, + data: this.data, }; } } -/** - * Enum for supported image MIME types. - */ export enum ChatImageMimeType { PNG = 'image/png', JPEG = 'image/jpeg', @@ -4973,11 +4996,25 @@ export enum ChatImageMimeType { BMP = 'image/bmp', } -export interface ChatImagePart { - mimeType: ChatImageMimeType; - data: VSBuffer; +export class LanguageModelExtraDataPart implements vscode.LanguageModelExtraDataPart { + kind: string; + data: any; + + constructor(kind: string, data: any) { + this.kind = kind; + this.data = data; + } + + toJSON() { + return { + $mid: MarshalledId.LanguageModelExtraDataPart, + kind: this.kind, + data: this.data, + }; + } } + export class LanguageModelPromptTsxPart { value: unknown; @@ -5074,6 +5111,17 @@ export class LanguageModelToolResult { } } +export class LanguageModelToolResult2 { + constructor(public content: (LanguageModelTextPart | LanguageModelPromptTsxPart | LanguageModelDataPart)[]) { } + + toJSON() { + return { + $mid: MarshalledId.LanguageModelToolResult, + content: this.content, + }; + } +} + export class ExtendedLanguageModelToolResult extends LanguageModelToolResult { } @@ -5093,6 +5141,12 @@ export enum RelatedInformationType { SettingInformation = 4 } +export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + //#endregion //#region Speech @@ -5142,15 +5196,17 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition public label: string, public command: string, public args: string[], - public env: Record + public env: Record = {}, + public version?: string, ) { } } -export class McpSSEServerDefinition implements vscode.McpSSEServerDefinition { - headers: [string, string][] = []; +export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { constructor( public label: string, - public uri: URI + public uri: URI, + public headers: Record = {}, + public version?: string, ) { } } //#endregion diff --git a/code/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/code/src/vs/workbench/api/common/extHostWebviewMessaging.ts index faf3d228656..49f0fa06c99 100644 --- a/code/src/vs/workbench/api/common/extHostWebviewMessaging.ts +++ b/code/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/code/src/vs/workbench/api/common/extHostWorkspace.ts b/code/src/vs/workbench/api/common/extHostWorkspace.ts index 4b6e88df490..b93ce34773d 100644 --- a/code/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/code/src/vs/workbench/api/common/extHostWorkspace.ts @@ -13,7 +13,7 @@ import { Schemas } from '../../../base/common/network.js'; import { Counter } from '../../../base/common/numbers.js'; import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from '../../../base/common/resources.js'; import { compare } from '../../../base/common/strings.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; +import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { FileSystemProviderCapabilities } from '../../../platform/files/common/files.js'; @@ -35,7 +35,10 @@ import { ExtHostWorkspaceShape, IRelativePatternDto, IWorkspaceData, MainContext import { revive } from '../../../base/common/marshalling.js'; import { AuthInfo, Credentials } from '../../../platform/request/common/request.js'; import { ExcludeSettingOptions, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; -import { VSBuffer } from '../../../base/common/buffer.js'; +import { bufferToStream, readableToBuffer, VSBuffer } from '../../../base/common/buffer.js'; +import { toDecodeStream, toEncodeReadable, UTF8 } from '../../services/textfile/common/encoding.js'; +import { consumeStream } from '../../../base/common/stream.js'; +import { stringToSnapshot } from '../../services/textfile/common/textfiles.js'; export interface IExtHostWorkspaceProvider { getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise; @@ -942,13 +945,47 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac // --- encodings --- - decode(content: Uint8Array, uri: UriComponents | undefined, options?: { encoding: string }): Promise { - return this._proxy.$decode(VSBuffer.wrap(content), uri, options); + async decode(content: Uint8Array, args?: { uri?: vscode.Uri; encoding?: string }): Promise { + const [uri, opts] = this.toEncodeDecodeParameters(args); + const options = await this._proxy.$resolveDecoding(uri, opts); + + const stream = (await toDecodeStream(bufferToStream(VSBuffer.wrap(content)), { + ...options, + acceptTextOnly: true, + overwriteEncoding: detectedEncoding => { + if (detectedEncoding === null || detectedEncoding === options.preferredEncoding) { + // Prevent another roundtrip to the main thread + // if the detected encoding is null or the same + // as the preferred encoding + return Promise.resolve(options.preferredEncoding); + } + + return this._proxy.$validateDetectedEncoding(uri, detectedEncoding, opts); + }, + })).stream; + + return consumeStream(stream, chunks => chunks.join('')); } - async encode(content: string, uri: UriComponents | undefined, options?: { encoding: string }): Promise { - const buff = await this._proxy.$encode(content, uri, options); - return buff.buffer; + async encode(content: string, args?: { uri?: vscode.Uri; encoding?: string }): Promise { + const [uri, options] = this.toEncodeDecodeParameters(args); + const { encoding, addBOM } = await this._proxy.$resolveEncoding(uri, options); + + // when encoding is standard skip encoding step + if (encoding === UTF8 && !addBOM) { + return VSBuffer.fromString(content).buffer; + } + + // otherwise create encoded readable + const res = await toEncodeReadable(stringToSnapshot(content), encoding, { addBOM }); + return readableToBuffer(res).buffer; + } + + private toEncodeDecodeParameters(opts?: { uri?: vscode.Uri; encoding?: string }): [UriComponents | undefined, { encoding: string } | undefined] { + const uri = isUriComponents(opts?.uri) ? opts.uri : undefined; + const encoding = typeof opts?.encoding === 'string' ? opts.encoding : undefined; + + return [uri, encoding ? { encoding } : undefined]; } } diff --git a/code/src/vs/workbench/api/node/extHost.node.services.ts b/code/src/vs/workbench/api/node/extHost.node.services.ts index 331d9a7b180..db7afe10529 100644 --- a/code/src/vs/workbench/api/node/extHost.node.services.ts +++ b/code/src/vs/workbench/api/node/extHost.node.services.ts @@ -28,7 +28,7 @@ import { ISignService } from '../../../platform/sign/common/sign.js'; import { SignService } from '../../../platform/sign/node/signService.js'; import { ExtHostTelemetry, IExtHostTelemetry } from '../common/extHostTelemetry.js'; import { IExtHostMpcService } from '../common/extHostMcp.js'; -import { NodeExtHostMpcService } from './extHostMpcNode.js'; +import { NodeExtHostMpcService } from './extHostMcpNode.js'; // ######################################################################### // ### ### diff --git a/code/src/vs/workbench/api/node/extHostExtensionService.ts b/code/src/vs/workbench/api/node/extHostExtensionService.ts index 7fae836bf43..edc1452c76d 100644 --- a/code/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/code/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.toString(true)); + } 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/code/src/vs/workbench/api/node/extHostMpcNode.ts b/code/src/vs/workbench/api/node/extHostMcpNode.ts similarity index 95% rename from code/src/vs/workbench/api/node/extHostMpcNode.ts rename to code/src/vs/workbench/api/node/extHostMcpNode.ts index 594fe45d0be..16e848a8d57 100644 --- a/code/src/vs/workbench/api/node/extHostMpcNode.ts +++ b/code/src/vs/workbench/api/node/extHostMcpNode.ts @@ -14,6 +14,7 @@ import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServer import { ExtHostMcpService } from '../common/extHostMcp.js'; import { IExtHostRpcService } from '../common/extHostRpcService.js'; import { findExecutable } from '../../../base/node/processes.js'; +import { untildify } from '../../../base/common/labels.js'; export class NodeExtHostMpcService extends ExtHostMcpService { constructor( @@ -57,6 +58,7 @@ export class NodeExtHostMpcService extends ExtHostMcpService { private async startNodeMpc(id: number, launch: McpServerTransportStdio) { const onError = (err: Error | string) => this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Error, + code: err.hasOwnProperty('code') ? String((err as any).code) : undefined, message: typeof err === 'string' ? err : err.message, }); @@ -80,8 +82,15 @@ export class NodeExtHostMpcService extends ExtHostMcpService { const abortCtrl = new AbortController(); let child: ChildProcessWithoutNullStreams; try { - const cwd = launch.cwd ? URI.revive(launch.cwd).fsPath : homedir(); - const { executable, args, shell } = await formatSubprocessArguments(launch.command, launch.args, cwd, env); + const home = homedir(); + const cwd = launch.cwd ? URI.revive(launch.cwd).fsPath : home; + const { executable, args, shell } = await formatSubprocessArguments( + untildify(launch.command, home), + launch.args.map(a => untildify(a, home)), + cwd, + env + ); + this._proxy.$onDidPublishLog(id, LogLevel.Debug, `Server command line: ${executable} ${args.join(' ')}`); child = spawn(executable, args, { stdio: 'pipe', diff --git a/code/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/code/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index d5fc283aeb6..bb5ff5e46ff 100644 --- a/code/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -6,10 +6,10 @@ import assert from 'assert'; import { MainThreadMessageService } from '../../browser/mainThreadMessageService.js'; import { IDialogService, IPrompt, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; -import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter, IStatusHandle } from '../../../../platform/notification/common/notification.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { mock } from '../../../../base/test/common/mock.js'; -import { IDisposable, Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; @@ -44,8 +44,8 @@ const emptyNotificationService = new class implements INotificationService { prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { throw new Error('not implemented'); } - status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } setFilter(): void { throw new Error('not implemented'); @@ -87,8 +87,8 @@ class EmptyNotificationService implements INotificationService { prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { throw new Error('Method not implemented'); } - status(message: string, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } setFilter(): void { throw new Error('Method not implemented.'); diff --git a/code/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/code/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 96c55513d54..8cd81152eac 100644 --- a/code/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -776,22 +776,6 @@ suite('ExtHostTypes', function () { assert.throws(() => types.FileDecoration.validate({ badge: 'ããã' })); }); - test('No longer possible to set content on LanguageModelChatMessage', function () { - const m = types.LanguageModelChatMessage.Assistant(''); - m.content = [new types.LanguageModelToolCallPart('toolCall.call.callId', 'toolCall.tool.name', 'toolCall.call.parameters')]; - - assert.equal(m.content.length, 1); - assert.equal(m.content2?.length, 1); - - - m.content2 = ['foo']; - assert.equal(m.content.length, 1); - assert.ok(m.content[0] instanceof types.LanguageModelTextPart); - - assert.equal(m.content2?.length, 1); - assert.ok(typeof m.content2[0] === 'string'); - }); - test('runtime stable, type-def changed', function () { // see https://github.com/microsoft/vscode/issues/231938 const m = new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, []); diff --git a/code/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/code/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index c13ac061a76..d7a3c6f68e8 100644 --- a/code/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/code/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -13,6 +13,7 @@ import { NullLogService } from '../../../../platform/log/common/log.js'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from '../../common/extHostExtensionActivator.js'; import { ExtensionDescriptionRegistry, IActivationEventsReader } from '../../../services/extensions/common/extensionDescriptionRegistry.js'; import { ExtensionActivationReason, MissingExtensionDependency } from '../../../services/extensions/common/extensions.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; suite('ExtensionsActivator', () => { @@ -23,26 +24,30 @@ suite('ExtensionsActivator', () => { const idC = new ExtensionIdentifier(`c`); test('calls activate only once with sequential activations', async () => { + const disposables = new DisposableStore(); const host = new SimpleExtensionsActivatorHost(); const activator = createActivator(host, [ desc(idA) - ]); + ], [], disposables); await activator.activateByEvent('*', false); assert.deepStrictEqual(host.activateCalls, [idA]); await activator.activateByEvent('*', false); assert.deepStrictEqual(host.activateCalls, [idA]); + + disposables.dispose(); }); test('calls activate only once with parallel activations', async () => { + const disposables = new DisposableStore(); const extActivation = new ExtensionActivationPromiseSource(); const host = new PromiseExtensionsActivatorHost([ [idA, extActivation] ]); const activator = createActivator(host, [ desc(idA, [], ['evt1', 'evt2']) - ]); + ], [], disposables); const activate1 = activator.activateByEvent('evt1', false); const activate2 = activator.activateByEvent('evt2', false); @@ -53,9 +58,12 @@ suite('ExtensionsActivator', () => { await activate2; assert.deepStrictEqual(host.activateCalls, [idA]); + + disposables.dispose(); }); test('activates dependencies first', async () => { + const disposables = new DisposableStore(); const extActivationA = new ExtensionActivationPromiseSource(); const extActivationB = new ExtensionActivationPromiseSource(); const host = new PromiseExtensionsActivatorHost([ @@ -65,7 +73,7 @@ suite('ExtensionsActivator', () => { const activator = createActivator(host, [ desc(idA, [idB], ['evt1']), desc(idB, [], ['evt1']), - ]); + ], [], disposables); const activate = activator.activateByEvent('evt1', false); @@ -81,22 +89,28 @@ suite('ExtensionsActivator', () => { await activate; assert.deepStrictEqual(host.activateCalls, [idB, idA]); + + disposables.dispose(); }); test('Supports having resolved extensions', async () => { + const disposables = new DisposableStore(); const host = new SimpleExtensionsActivatorHost(); const bExt = desc(idB); delete (>bExt).main; delete (>bExt).browser; const activator = createActivator(host, [ desc(idA, [idB]) - ], [bExt]); + ], [bExt], disposables); await activator.activateByEvent('*', false); assert.deepStrictEqual(host.activateCalls, [idA]); + + disposables.dispose(); }); test('Supports having external extensions', async () => { + const disposables = new DisposableStore(); const extActivationA = new ExtensionActivationPromiseSource(); const extActivationB = new ExtensionActivationPromiseSource(); const host = new PromiseExtensionsActivatorHost([ @@ -107,7 +121,7 @@ suite('ExtensionsActivator', () => { (>bExt).api = 'none'; const activator = createActivator(host, [ desc(idA, [idB]) - ], [bExt]); + ], [bExt], disposables); const activate = activator.activateByEvent('*', false); @@ -121,14 +135,17 @@ suite('ExtensionsActivator', () => { await activate; assert.deepStrictEqual(host.activateCalls, [idB, idA]); + + disposables.dispose(); }); test('Error: activateById with missing extension', async () => { + const disposables = new DisposableStore(); const host = new SimpleExtensionsActivatorHost(); const activator = createActivator(host, [ desc(idA), desc(idB), - ]); + ], [], disposables); let error: Error | undefined = undefined; try { @@ -138,21 +155,27 @@ suite('ExtensionsActivator', () => { } assert.strictEqual(typeof error === 'undefined', false); + + disposables.dispose(); }); test('Error: dependency missing', async () => { + const disposables = new DisposableStore(); const host = new SimpleExtensionsActivatorHost(); const activator = createActivator(host, [ desc(idA, [idB]), - ]); + ], [], disposables); await activator.activateByEvent('*', false); assert.deepStrictEqual(host.errors.length, 1); assert.deepStrictEqual(host.errors[0][0], idA); + + disposables.dispose(); }); test('Error: dependency activation failed', async () => { + const disposables = new DisposableStore(); const extActivationA = new ExtensionActivationPromiseSource(); const extActivationB = new ExtensionActivationPromiseSource(); const host = new PromiseExtensionsActivatorHost([ @@ -162,7 +185,7 @@ suite('ExtensionsActivator', () => { const activator = createActivator(host, [ desc(idA, [idB]), desc(idB) - ]); + ], [], disposables); const activate = activator.activateByEvent('*', false); extActivationB.reject(new Error(`b fails!`)); @@ -171,9 +194,12 @@ suite('ExtensionsActivator', () => { assert.deepStrictEqual(host.errors.length, 2); assert.deepStrictEqual(host.errors[0][0], idB); assert.deepStrictEqual(host.errors[1][0], idA); + + disposables.dispose(); }); test('issue #144518: Problem with git extension and vscode-icons', async () => { + const disposables = new DisposableStore(); const extActivationA = new ExtensionActivationPromiseSource(); const extActivationB = new ExtensionActivationPromiseSource(); const extActivationC = new ExtensionActivationPromiseSource(); @@ -186,7 +212,7 @@ suite('ExtensionsActivator', () => { desc(idA, [idB]), desc(idB), desc(idC), - ]); + ], [], disposables); activator.activateByEvent('*', false); assert.deepStrictEqual(host.activateCalls, [idB, idC]); @@ -196,6 +222,8 @@ suite('ExtensionsActivator', () => { assert.deepStrictEqual(host.activateCalls, [idB, idC, idA]); extActivationA.resolve(); + + disposables.dispose(); }); class SimpleExtensionsActivatorHost implements IExtensionsActivatorHost { @@ -255,10 +283,10 @@ suite('ExtensionsActivator', () => { } }; - function createActivator(host: IExtensionsActivatorHost, extensionDescriptions: IExtensionDescription[], otherHostExtensionDescriptions: IExtensionDescription[] = []): ExtensionsActivator { - const registry = new ExtensionDescriptionRegistry(basicActivationEventsReader, extensionDescriptions); - const globalRegistry = new ExtensionDescriptionRegistry(basicActivationEventsReader, extensionDescriptions.concat(otherHostExtensionDescriptions)); - return new ExtensionsActivator(registry, globalRegistry, host, new NullLogService()); + function createActivator(host: IExtensionsActivatorHost, extensionDescriptions: IExtensionDescription[], otherHostExtensionDescriptions: IExtensionDescription[] = [], disposables: DisposableStore): ExtensionsActivator { + const registry = disposables.add(new ExtensionDescriptionRegistry(basicActivationEventsReader, extensionDescriptions)); + const globalRegistry = disposables.add(new ExtensionDescriptionRegistry(basicActivationEventsReader, extensionDescriptions.concat(otherHostExtensionDescriptions))); + return disposables.add(new ExtensionsActivator(registry, globalRegistry, host, new NullLogService())); } function desc(id: ExtensionIdentifier, deps: ExtensionIdentifier[] = [], activationEvents: string[] = ['*']): IExtensionDescription { diff --git a/code/src/vs/workbench/api/test/node/extHostSearch.test.ts b/code/src/vs/workbench/api/test/node/extHostSearch.test.ts index 94380c10ec4..4abd86979a5 100644 --- a/code/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/code/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -26,6 +26,7 @@ import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteSt import { TextSearchManager } from '../../../services/search/common/textSearchManager.js'; import { NativeTextSearchManager } from '../../../services/search/node/textSearchManager.js'; import type * as vscode from 'vscode'; +import { AISearchKeyword } from '../../../services/search/common/searchExtTypes.js'; let rpcProtocol: TestRPCProtocol; let extHostSearch: NativeExtHostSearch; @@ -36,6 +37,8 @@ class MockMainThreadSearch implements MainThreadSearchShape { results: Array = []; + keywords: Array = []; + $registerFileSearchProvider(handle: number, scheme: string): void { this.lastHandle = handle; } @@ -59,6 +62,10 @@ class MockMainThreadSearch implements MainThreadSearchShape { this.results.push(...data); } + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void { + this.keywords.push(data); + } + $handleTelemetry(eventName: string, data: any): void { } diff --git a/code/src/vs/workbench/api/worker/extHostExtensionService.ts b/code/src/vs/workbench/api/worker/extHostExtensionService.ts index 6f3384ab89c..46bb2a0970f 100644 --- a/code/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/code/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/code/src/vs/workbench/browser/actions/helpActions.ts b/code/src/vs/workbench/browser/actions/helpActions.ts index efda59bfc12..2487213aa2d 100644 --- a/code/src/vs/workbench/browser/actions/helpActions.ts +++ b/code/src/vs/workbench/browser/actions/helpActions.ts @@ -9,13 +9,14 @@ import { isMacintosh, isLinux, language, isWeb } from '../../../base/common/plat import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { URI } from '../../../base/common/uri.js'; -import { MenuId, Action2, registerAction2 } from '../../../platform/actions/common/actions.js'; +import { MenuId, Action2, registerAction2, MenuRegistry } from '../../../platform/actions/common/actions.js'; import { KeyChord, KeyMod, KeyCode } from '../../../base/common/keyCodes.js'; import { IProductService } from '../../../platform/product/common/productService.js'; import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js'; class KeybindingsReferenceAction extends Action2 { @@ -279,7 +280,7 @@ class OpenLicenseUrlAction extends Action2 { class OpenPrivacyStatementUrlAction extends Action2 { static readonly ID = 'workbench.action.openPrivacyStatementUrl'; - static readonly AVAILABE = !!product.privacyStatementUrl; + static readonly AVAILABLE = !!product.privacyStatementUrl; constructor() { super({ @@ -331,30 +332,35 @@ class GetStartedWithAccessibilityFeatures extends Action2 { } } -class GetStartedWithCopilot extends Action2 { - - static readonly ID = 'workbench.action.getStartedWithCopilot'; - static readonly AVAILABE = !!product.defaultChatAgent?.documentationUrl; +class AskVSCodeCopilot extends Action2 { + static readonly ID = 'workbench.action.askVScode'; constructor() { super({ - id: GetStartedWithCopilot.ID, - title: localize2('getStartedWithCopilot', 'Get Started with Copilot'), + id: AskVSCodeCopilot.ID, + title: localize2('askVScode', 'Ask @vscode'), category: Categories.Help, f1: true, - menu: { - id: MenuId.MenubarHelpMenu, - group: '1_welcome', - order: 7 - } + precondition: ContextKeyExpr.equals('chatSetupHidden', false) }); } - run(accessor: ServicesAccessor): void { - const openerService = accessor.get(IOpenerService); - openerService.open(URI.parse(product.defaultChatAgent!.documentationUrl)); + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + commandService.executeCommand('workbench.action.chat.open', { mode: 'ask', query: '@vscode ', isPartialQuery: true }); } } +MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + command: { + id: AskVSCodeCopilot.ID, + title: localize2('askVScode', 'Ask @vscode'), + }, + order: 7, + group: '1_welcome', + when: ContextKeyExpr.equals('chatSetupHidden', false) +}); + // --- Actions Registration if (KeybindingsReferenceAction.AVAILABLE) { @@ -389,12 +395,10 @@ if (OpenLicenseUrlAction.AVAILABLE) { registerAction2(OpenLicenseUrlAction); } -if (OpenPrivacyStatementUrlAction.AVAILABE) { +if (OpenPrivacyStatementUrlAction.AVAILABLE) { registerAction2(OpenPrivacyStatementUrlAction); } registerAction2(GetStartedWithAccessibilityFeatures); -if (GetStartedWithCopilot.AVAILABE) { - registerAction2(GetStartedWithCopilot); -} +registerAction2(AskVSCodeCopilot); diff --git a/code/src/vs/workbench/browser/actions/layoutActions.ts b/code/src/vs/workbench/browser/actions/layoutActions.ts index fd3a6842589..88d98a9f0ce 100644 --- a/code/src/vs/workbench/browser/actions/layoutActions.ts +++ b/code/src/vs/workbench/browser/actions/layoutActions.ts @@ -22,7 +22,7 @@ import { IPaneCompositePartService } from '../../services/panecomposite/browser/ import { ToggleAuxiliaryBarAction } from '../parts/auxiliarybar/auxiliaryBarActions.js'; import { TogglePanelAction } from '../parts/panel/panelActions.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; -import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, IsMainWindowFullscreenContext, PanelPositionContext, IsAuxiliaryWindowFocusedContext, TitleBarStyleContext } from '../../common/contextkeys.js'; +import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, IsMainWindowFullscreenContext, PanelPositionContext, IsAuxiliaryWindowFocusedContext, TitleBarStyleContext, IsAuxiliaryTitleBarContext } from '../../common/contextkeys.js'; import { Codicon } from '../../../base/common/codicons.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; @@ -173,7 +173,10 @@ MenuRegistry.appendMenuItem(MenuId.LayoutControlMenu, { title: localize('configureLayout', "Configure Layout"), icon: configureLayoutIcon, group: '1_workbench_layout', - when: ContextKeyExpr.equals('config.workbench.layoutControl.type', 'menu') + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'menu') + ) }); @@ -345,7 +348,13 @@ MenuRegistry.appendMenuItems([ icon: panelLeftOffIcon, toggled: { condition: SideBarVisibleContext, icon: panelLeftIcon } }, - when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'left')), + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), + ContextKeyExpr.equals('config.workbench.sideBar.location', 'left') + ), order: 0 } }, { @@ -358,7 +367,13 @@ MenuRegistry.appendMenuItems([ icon: panelRightOffIcon, toggled: { condition: SideBarVisibleContext, icon: panelRightIcon } }, - when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'right')), + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), + ContextKeyExpr.equals('config.workbench.sideBar.location', 'right') + ), order: 2 } } @@ -1433,7 +1448,10 @@ registerAction2(class CustomizeLayoutAction extends Action2 { }, { id: MenuId.LayoutControlMenu, - when: ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both'), + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.toNegated(), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both') + ), group: '1_layout' } ] diff --git a/code/src/vs/workbench/browser/contextkeys.ts b/code/src/vs/workbench/browser/contextkeys.ts index e5192e1dd69..840174189ee 100644 --- a/code/src/vs/workbench/browser/contextkeys.ts +++ b/code/src/vs/workbench/browser/contextkeys.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../base/common/event.js'; -import { Disposable } from '../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../base/common/lifecycle.js'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from '../../platform/contextkey/common/contextkey.js'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from '../../platform/contextkey/common/contextkeys.js'; import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext } from '../common/contextkeys.js'; @@ -216,7 +216,7 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); - this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document), true)), { window: mainWindow, disposables: this._store })); + this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document, disposables), true)), { window: mainWindow, disposables: this._store })); this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateWorkbenchStateContextKey())); this._register(this.contextService.onDidChangeWorkspaceFolders(() => { @@ -297,7 +297,7 @@ export class WorkbenchContextKeysHandler extends Disposable { this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); } - private updateInputContextKeys(ownerDocument: Document): void { + private updateInputContextKeys(ownerDocument: Document, disposables: DisposableStore): void { function activeElementIsInput(): boolean { return !!ownerDocument.activeElement && isEditableElement(ownerDocument.activeElement); @@ -307,7 +307,7 @@ export class WorkbenchContextKeysHandler extends Disposable { this.inputFocusedContext.set(isInputFocused); if (isInputFocused) { - const tracker = trackFocus(ownerDocument.activeElement as HTMLElement); + const tracker = disposables.add(trackFocus(ownerDocument.activeElement as HTMLElement)); Event.once(tracker.onDidBlur)(() => { // Ensure we are only updating the context key if we are @@ -323,7 +323,7 @@ export class WorkbenchContextKeysHandler extends Disposable { } tracker.dispose(); - }); + }, undefined, disposables); } } diff --git a/code/src/vs/workbench/browser/dnd.ts b/code/src/vs/workbench/browser/dnd.ts index 72def8c4b99..16bddbd0ea1 100644 --- a/code/src/vs/workbench/browser/dnd.ts +++ b/code/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/code/src/vs/workbench/browser/layout.ts b/code/src/vs/workbench/browser/layout.ts index bd0a9496f84..0c3fbf7525b 100644 --- a/code/src/vs/workbench/browser/layout.ts +++ b/code/src/vs/workbench/browser/layout.ts @@ -363,6 +363,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION, LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE, ].some(setting => e.affectsConfiguration(setting))) { + // Show Command Center if command center actions enabled const shareEnabled = e.affectsConfiguration('workbench.experimental.share.enabled') && this.configurationService.getValue('workbench.experimental.share.enabled'); const navigationControlEnabled = e.affectsConfiguration('workbench.navigationControl.enabled') && this.configurationService.getValue('workbench.navigationControl.enabled'); @@ -1047,10 +1048,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi mark('code/willRestoreViewlet'); - const viewlet = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.sideBar, ViewContainerLocation.Sidebar); - if (!viewlet) { - await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar); // fallback to default viewlet as needed - } + await this.openViewContainer(ViewContainerLocation.Sidebar, this.state.initialization.views.containerToRestore.sideBar); mark('code/didRestoreViewlet'); })()); @@ -1067,10 +1065,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi mark('code/willRestorePanel'); - const panel = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.panel, ViewContainerLocation.Panel); - if (!panel) { - await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id, ViewContainerLocation.Panel); // fallback to default panel as needed - } + await this.openViewContainer(ViewContainerLocation.Panel, this.state.initialization.views.containerToRestore.panel); mark('code/didRestorePanel'); })()); @@ -1087,10 +1082,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi mark('code/willRestoreAuxiliaryBar'); - const viewlet = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); - if (!viewlet) { - await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default viewlet as needed - } + await this.openViewContainer(ViewContainerLocation.AuxiliaryBar, this.state.initialization.views.containerToRestore.auxiliaryBar); mark('code/didRestoreAuxiliaryBar'); })()); @@ -1121,6 +1113,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }); } + private async openViewContainer(location: ViewContainerLocation, id: string, focus?: boolean): Promise { + let viewContainer = await this.paneCompositeService.openPaneComposite(id, location, focus); + if (viewContainer) { + return; + } + + // fallback to default view container + viewContainer = await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(location)?.id, location, focus); + if (viewContainer) { + return; + } + + // finally try to just open the first visible view container + await this.paneCompositeService.openPaneComposite(this.paneCompositeService.getVisiblePaneCompositeIds(location).at(0), location, focus); + } + registerPart(part: Part): IDisposable { const id = part.getId(); this.parts.set(id, part); @@ -1234,32 +1242,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return true; // cannot hide editor part in auxiliary windows } - if (this.initialized) { - switch (part) { - case Parts.TITLEBAR_PART: - return this.workbenchGrid.isViewVisible(this.titleBarPartView); - case Parts.SIDEBAR_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); - case Parts.PANEL_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN); - case Parts.AUXILIARYBAR_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN); - case Parts.STATUSBAR_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN); - case Parts.ACTIVITYBAR_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); - case Parts.EDITOR_PART: - return !this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN); - case Parts.BANNER_PART: - return this.workbenchGrid.isViewVisible(this.bannerPartView); - default: - return false; // any other part cannot be hidden - } - } - switch (part) { case Parts.TITLEBAR_PART: - return shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled); + return this.initialized ? + this.workbenchGrid.isViewVisible(this.titleBarPartView) : + shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled); case Parts.SIDEBAR_PART: return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); case Parts.PANEL_PART: @@ -1272,6 +1259,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); case Parts.EDITOR_PART: return !this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN); + case Parts.BANNER_PART: + return this.initialized ? this.workbenchGrid.isViewVisible(this.bannerPartView) : false; default: return false; // any other part cannot be hidden } @@ -1381,14 +1370,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setPanelHidden(true, true); this.setAuxiliaryBarHidden(true, true); - this.setSideBarHidden(true, true); + this.setSideBarHidden(true); if (config.hideActivityBar) { - this.setActivityBarHidden(true, true); + this.setActivityBarHidden(true); } if (config.hideStatusBar) { - this.setStatusBarHidden(true, true); + this.setStatusBarHidden(true); } if (config.hideLineNumbers) { @@ -1413,13 +1402,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Activity Bar if (e.affectsConfiguration(ZenModeSettings.HIDE_ACTIVITYBAR)) { const zenModeHideActivityBar = this.configurationService.getValue(ZenModeSettings.HIDE_ACTIVITYBAR); - this.setActivityBarHidden(zenModeHideActivityBar, true); + this.setActivityBarHidden(zenModeHideActivityBar); } // Status Bar if (e.affectsConfiguration(ZenModeSettings.HIDE_STATUSBAR)) { const zenModeHideStatusBar = this.configurationService.getValue(ZenModeSettings.HIDE_STATUSBAR); - this.setStatusBarHidden(zenModeHideStatusBar, true); + this.setStatusBarHidden(zenModeHideStatusBar); } // Center Layout @@ -1462,15 +1451,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (zenModeExitInfo.wasVisible.sideBar) { - this.setSideBarHidden(false, true); + this.setSideBarHidden(false); } if (!this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN, true)) { - this.setActivityBarHidden(false, true); + this.setActivityBarHidden(false); } if (!this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN, true)) { - this.setStatusBarHidden(false, true); + this.setStatusBarHidden(false); } if (zenModeExitInfo.transitionedToCenteredEditorLayout) { @@ -1509,7 +1498,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this._onDidChangeZenMode.fire(this.isZenModeActive()); } - private setStatusBarHidden(hidden: boolean, skipLayout?: boolean): void { + private setStatusBarHidden(hidden: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN, hidden); // Adjust CSS @@ -1569,13 +1558,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi for (const part of [titleBar, editorPart, activityBar, panelPart, sideBar, statusBar, auxiliaryBarPart, bannerPart]) { this._register(part.onDidVisibilityChange((visible) => { if (part === sideBar) { - this.setSideBarHidden(!visible, true); + this.setSideBarHidden(!visible); } else if (part === panelPart) { this.setPanelHidden(!visible, true); } else if (part === auxiliaryBarPart) { this.setAuxiliaryBarHidden(!visible, true); } else if (part === editorPart) { - this.setEditorHidden(!visible, true); + this.setEditorHidden(!visible); } this._onDidChangePartVisibility.fire(); this.handleContainerDidLayout(this.mainContainer, this._mainContainerDimension); @@ -1753,7 +1742,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - private setActivityBarHidden(hidden: boolean, skipLayout?: boolean): void { + private setActivityBarHidden(hidden: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN, hidden); this.workbenchGrid.setViewVisible(this.activityBarPartView, !hidden); } @@ -1762,7 +1751,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.setViewVisible(this.bannerPartView, !hidden); } - private setEditorHidden(hidden: boolean, skipLayout?: boolean): void { + private setEditorHidden(hidden: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN, hidden); // Adjust CSS @@ -1792,7 +1781,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi ]); } - private setSideBarHidden(hidden: boolean, skipLayout?: boolean): void { + private setSideBarHidden(hidden: boolean): void { this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, hidden); // Adjust CSS @@ -1812,10 +1801,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { - const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar, true); - if (!viewlet) { - this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar, true); - } + this.openViewContainer(ViewContainerLocation.Sidebar, viewletToOpen, true); } } @@ -1900,7 +1886,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - setPanelAlignment(alignment: PanelAlignment, skipLayout?: boolean): void { + setPanelAlignment(alignment: PanelAlignment): void { // Panel alignment only applies to a panel in the top/bottom position if (!isHorizontal(this.getPanelPosition())) { @@ -1960,11 +1946,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (panelToOpen) { - const focus = !skipLayout; - const panel = this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); - if (!panel) { - this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id, ViewContainerLocation.Panel, focus); - } + this.openViewContainer(ViewContainerLocation.Panel, panelToOpen, !skipLayout); } } @@ -2063,11 +2045,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (viewletToOpen) { - const focus = !skipLayout; - const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.AuxiliaryBar, focus); - if (!viewlet) { - this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar, focus); - } + this.openViewContainer(ViewContainerLocation.AuxiliaryBar, viewletToOpen, !skipLayout); } } @@ -2075,9 +2053,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, !hidden); } - setPartHidden(hidden: boolean, part: Exclude): void; - setPartHidden(hidden: boolean, part: Exclude, targetWindow: Window): void; - setPartHidden(hidden: boolean, part: Parts, targetWindow: Window = mainWindow): void { + setPartHidden(hidden: boolean, part: Parts): void { switch (part) { case Parts.ACTIVITYBAR_PART: return this.setActivityBarHidden(hidden); @@ -2636,6 +2612,8 @@ interface ILayoutStateChangeEvent { } enum WorkbenchLayoutSettings { + AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility', + ACTIVITY_BAR_VISIBLE = 'workbench.activityBar.visible', PANEL_POSITION = 'workbench.panel.defaultLocation', PANEL_OPENS_MAXIMIZED = 'workbench.panel.opensMaximized', ZEN_MODE_CONFIG = 'zenMode', @@ -2643,8 +2621,8 @@ enum WorkbenchLayoutSettings { } enum LegacyWorkbenchLayoutSettings { - STATUSBAR_VISIBLE = 'workbench.statusBar.visible', // Deprecated to UI State - SIDEBAR_POSITION = 'workbench.sideBar.location', // Deprecated to UI State + STATUSBAR_VISIBLE = 'workbench.statusBar.visible', // Deprecated to UI State + SIDEBAR_POSITION = 'workbench.sideBar.location', // Deprecated to UI State } class LayoutStateModel extends Disposable { @@ -2714,11 +2692,22 @@ class LayoutStateModel extends Disposable { this.stateCache.set(LayoutStateKeys.SIDEBAR_POSITON.name, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); // Set dynamic defaults: part sizing and side bar visibility - LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); + const workbenchState = this.contextService.getWorkbenchState(); LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY; LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); + LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => { + switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) { + case 'visible': + return false; + case 'visibleInWorkspace': + return workbenchState === WorkbenchState.EMPTY; + default: + return true; + } + })(); LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? mainContainerDimension.height / 3 : mainContainerDimension.width / 4; - LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; + LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); // Apply all defaults for (key in LayoutStateKeys) { @@ -2803,7 +2792,7 @@ class LayoutStateModel extends Disposable { } private isActivityBarHidden(): boolean { - const oldValue = this.configurationService.getValue('workbench.activityBar.visible'); + const oldValue = this.configurationService.getValue(WorkbenchLayoutSettings.ACTIVITY_BAR_VISIBLE); if (oldValue !== undefined) { return !oldValue; } diff --git a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index e5cc8c36550..72f165f7f00 100644 --- a/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/code/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -9,7 +9,7 @@ import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../plat import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { AuxiliaryBarVisibleContext } from '../../../common/contextkeys.js'; +import { AuxiliaryBarVisibleContext, IsAuxiliaryTitleBarContext } from '../../../common/contextkeys.js'; import { ViewContainerLocation, ViewContainerLocationToString } from '../../../common/views.js'; import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts } from '../../../services/layout/browser/layoutService.js'; import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; @@ -132,7 +132,13 @@ MenuRegistry.appendMenuItems([ toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon }, icon: auxiliaryBarLeftOffIcon, }, - when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'right')), + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), + ContextKeyExpr.equals('config.workbench.sideBar.location', 'right') + ), order: 0 } }, { @@ -145,7 +151,13 @@ MenuRegistry.appendMenuItems([ toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon }, icon: auxiliaryBarRightOffIcon, }, - when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'left')), + when: ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), + ContextKeyExpr.equals('config.workbench.sideBar.location', 'left') + ), order: 2 } }, { diff --git a/code/src/vs/workbench/browser/parts/banner/bannerPart.ts b/code/src/vs/workbench/browser/parts/banner/bannerPart.ts index 8495bd425bc..aa0ed529deb 100644 --- a/code/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/code/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/code/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/code/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index ac461ef9438..d4db3411faf 100644 --- a/code/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/code/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/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index 4a001a815ec..59276b6858f 100644 --- a/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/code/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { onDidChangeFullscreen } from '../../../../base/browser/browser.js'; -import { $, hide, show } from '../../../../base/browser/dom.js'; +import { $, getActiveWindow, hide, show } from '../../../../base/browser/dom.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isNative } from '../../../../base/common/platform.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; @@ -27,6 +27,11 @@ import { IWorkbenchLayoutService, shouldShowCustomTitleBar } from '../../../serv import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { ITitleService } from '../../../services/title/browser/titleService.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { IsAuxiliaryTitleBarContext, IsAuxiliaryWindowFocusedContext, IsCompactTitleBarContext } from '../../../common/contextkeys.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; export interface IAuxiliaryEditorPartOpenOptions extends IAuxiliaryWindowOpenOptions { readonly state?: IEditorPartUIState; @@ -38,6 +43,66 @@ export interface ICreateAuxiliaryEditorPartResult { readonly disposables: DisposableStore; } +const compactWindowEmitter = markAsSingleton(new Emitter<{ windowId: number; compact: boolean | 'toggle' }>()); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.toggleCompactAuxiliaryWindow', + title: localize2('toggleCompactAuxiliaryWindow', "Toggle Window Compact Mode"), + category: Categories.View, + f1: true, + precondition: IsAuxiliaryWindowFocusedContext + }); + } + + override async run(): Promise { + compactWindowEmitter.fire({ windowId: getActiveWindow().vscodeWindowId, compact: 'toggle' }); + } +}); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.enableCompactAuxiliaryWindow', + title: localize('enableCompactAuxiliaryWindow', "Set Compact Mode"), + icon: Codicon.screenFull, + menu: { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and(IsAuxiliaryTitleBarContext, IsCompactTitleBarContext.toNegated()), + order: 0 + } + }); + } + + override async run(): Promise { + compactWindowEmitter.fire({ windowId: getActiveWindow().vscodeWindowId, compact: true }); + } +}); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.action.disableCompactAuxiliaryWindow', + title: localize('disableCompactAuxiliaryWindow', "Unset Compact Mode"), + icon: Codicon.screenNormal, + toggled: ContextKeyExpr.and(IsAuxiliaryTitleBarContext, IsCompactTitleBarContext), + menu: { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and(IsAuxiliaryTitleBarContext, IsCompactTitleBarContext), + order: 0 + } + }); + } + + override async run(): Promise { + compactWindowEmitter.fire({ windowId: getActiveWindow().vscodeWindowId, compact: false }); + } +}); + export class AuxiliaryEditorPart { private static STATUS_BAR_VISIBILITY = 'workbench.statusBar.visible'; @@ -56,6 +121,10 @@ export class AuxiliaryEditorPart { } async create(label: string, options?: IAuxiliaryEditorPartOpenOptions): Promise { + const that = this; + const disposables = new DisposableStore(); + + let compact = Boolean(options?.compact); function computeEditorPartHeightOffset(): number { let editorPartHeightOffset = 0; @@ -99,7 +168,22 @@ export class AuxiliaryEditorPart { } } - const disposables = new DisposableStore(); + function updateCompact(newCompact: boolean): void { + if (newCompact === compact) { + return; + } + + compact = newCompact; + auxiliaryWindow.updateOptions({ compact }); + titlebarPart?.updateOptions({ compact }); + editorPart.updateOptions({ compact }); + + const oldStatusbarVisible = statusbarVisible; + statusbarVisible = !compact && that.configurationService.getValue(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY) !== false; + if (oldStatusbarVisible !== statusbarVisible) { + updateStatusbarVisibility(true); + } + } // Auxiliary Window const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open(options)); @@ -110,6 +194,7 @@ export class AuxiliaryEditorPart { auxiliaryWindow.container.appendChild(editorPartContainer); const editorPart = disposables.add(this.instantiationService.createInstance(AuxiliaryEditorPartImpl, auxiliaryWindow.window.vscodeWindowId, this.editorPartsView, options?.state, label)); + editorPart.updateOptions({ compact }); disposables.add(this.editorPartsView.registerPart(editorPart)); editorPart.create(editorPartContainer); @@ -119,6 +204,7 @@ export class AuxiliaryEditorPart { const useCustomTitle = isNative && hasCustomTitlebar(this.configurationService); // custom title in aux windows only enabled in native if (useCustomTitle) { titlebarPart = disposables.add(this.titleService.createAuxiliaryTitlebarPart(auxiliaryWindow.container, editorPart)); + titlebarPart.updateOptions({ compact }); titlebarVisible = shouldShowCustomTitleBar(this.configurationService, auxiliaryWindow.window, undefined); const handleTitleBarVisibilityEvent = () => { @@ -146,10 +232,10 @@ export class AuxiliaryEditorPart { // Statusbar const statusbarPart = disposables.add(this.statusbarService.createAuxiliaryStatusbarPart(auxiliaryWindow.container)); - let statusbarVisible = this.configurationService.getValue(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY) !== false; + let statusbarVisible = !compact && this.configurationService.getValue(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY) !== false; disposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY)) { - statusbarVisible = this.configurationService.getValue(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY) !== false; + statusbarVisible = !compact && this.configurationService.getValue(AuxiliaryEditorPart.STATUS_BAR_VISIBILITY) !== false; updateStatusbarVisibility(true); } @@ -200,7 +286,20 @@ export class AuxiliaryEditorPart { })); auxiliaryWindow.layout(); - // Have a InstantiationService that is scoped to the auxiliary window + // Compact mode + disposables.add(compactWindowEmitter.event(e => { + if (e.windowId === auxiliaryWindow.window.vscodeWindowId) { + let newCompact: boolean; + if (typeof e.compact === 'boolean') { + newCompact = e.compact; + } else { + newCompact = !compact; + } + updateCompact(newCompact); + } + })); + + // Have a scoped instantiation service that is scoped to the auxiliary window const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection( [IStatusbarService, this.statusbarService.createScoped(statusbarPart, disposables)], [IEditorService, this.editorService.createScoped(editorPart, disposables)] @@ -221,6 +320,8 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart private readonly _onWillClose = this._register(new Emitter()); readonly onWillClose = this._onWillClose.event; + private readonly optionsDisposable = this._register(new MutableDisposable()); + constructor( windowId: number, editorPartsView: IEditorPartsView, @@ -238,6 +339,16 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart super(editorPartsView, `workbench.parts.auxiliaryEditor.${id}`, groupsLabel, windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService); } + updateOptions(options: { compact: boolean }): void { + if (options.compact && !this.optionsDisposable.value) { + this.optionsDisposable.value = this.enforcePartOptions({ + showTabs: 'none' + }); + } else if (!options.compact) { + this.optionsDisposable.clear(); + } + } + override removeGroup(group: number | IEditorGroupView, preserveFocus?: boolean): void { // Close aux window when last group removed @@ -266,7 +377,7 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart } } - this.doClose(false /* do not merge any groups to main part */); + this.doClose(false /* do not merge any confirming editors to main part */); } protected override loadState(): IEditorPartUIState | undefined { @@ -278,12 +389,19 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart } close(): boolean { - return this.doClose(true /* merge all groups to main part */); + return this.doClose(true /* merge all confirming editors to main part */); } - private doClose(mergeGroupsToMainPart: boolean): boolean { + private doClose(mergeConfirmingEditorsToMainPart: boolean): boolean { let result = true; - if (mergeGroupsToMainPart) { + if (mergeConfirmingEditorsToMainPart) { + + // First close all editors that are non-confirming + for (const group of this.groups) { + group.closeAllEditors({ excludeConfirming: true }); + } + + // Then merge remaining to main part result = this.mergeGroupsToMainPart(); } diff --git a/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 351252cecda..838d74fd56f 100644 --- a/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/code/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -290,6 +290,7 @@ export class BreadcrumbsControl { dispose(): void { this._disposables.dispose(); this._breadcrumbsDisposables.dispose(); + this._model.dispose(); this._ckBreadcrumbsPossible.reset(); this._ckBreadcrumbsVisible.reset(); this._ckBreadcrumbsActive.reset(); diff --git a/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index dc3f83afe6c..df972d58710 100644 --- a/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/code/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -16,6 +16,7 @@ import { TextDiffEditor } from './textDiffEditor.js'; import { ActiveCompareEditorCanSwapContext, TextCompareEditorActiveContext, TextCompareEditorVisibleContext } from '../../../common/contextkeys.js'; import { DiffEditorInput } from '../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IUntypedEditorInput } from '../../../common/editor.js'; export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; @@ -150,14 +151,14 @@ export function registerDiffEditorCommands(): void { // yet opened. This ensures that the swapping is not // bringing up a confirmation dialog to save. if (diffInput.modified.isModified() && editorService.findEditors({ resource: diffInput.modified.resource, typeId: diffInput.modified.typeId, editorId: diffInput.modified.editorId }).length === 0) { - await editorService.openEditor({ - ...untypedDiffInput.modified, - options: { - ...untypedDiffInput.modified.options, - pinned: true, - inactive: true - } - }, activeGroup); + const editorToOpen: IUntypedEditorInput = { ...untypedDiffInput.modified }; + if (!editorToOpen.options) { + editorToOpen.options = {}; + } + editorToOpen.options.pinned = true; + editorToOpen.options.inactive = true; + + await editorService.openEditor(editorToOpen, activeGroup); } // Replace the input with the swapped variant diff --git a/code/src/vs/workbench/browser/parts/editor/editorActions.ts b/code/src/vs/workbench/browser/parts/editor/editorActions.ts index 52d1dc5859c..d7452326923 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -586,9 +586,17 @@ abstract class AbstractCloseAllAction extends Action2 { for (const { editor, groupId } of editorService.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: this.excludeSticky })) { let confirmClose = false; + let handlerDidError = false; if (editor.closeHandler) { - confirmClose = editor.closeHandler.showConfirm(); // custom handling of confirmation on close - } else { + try { + confirmClose = editor.closeHandler.showConfirm(); // custom handling of confirmation on close + } catch (error) { + logService.error(error); + handlerDidError = true; + } + } + + if (!editor.closeHandler || handlerDidError) { confirmClose = editor.isDirty() && !editor.isSaving(); // default confirm only when dirty and not saving } diff --git a/code/src/vs/workbench/browser/parts/editor/editorCommands.ts b/code/src/vs/workbench/browser/parts/editor/editorCommands.ts index 83a724a79b1..9463dfc1de3 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/code/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,73 +868,87 @@ 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); + handler: (accessor, ...args: unknown[]) => { + return reopenEditorWith(accessor, EditorResolution.PICK, ...args); + } + }); - const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); - const editorReplacements = new Map(); + 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); + } + }); - 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 - } + 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); - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } + const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); + const editorReplacements = new Map(); - let editorReplacementsInGroup = editorReplacements.get(group); - if (!editorReplacementsInGroup) { - editorReplacementsInGroup = []; - editorReplacements.set(group, editorReplacementsInGroup); - } + 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 + } - 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 ?? '' - }); + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: editorOverride }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; } - } - // Replace editor with resolved one and make active - for (const [group, replacements] of editorReplacements) { - await group.replaceEditors(replacements); - await group.openEditor(replacements[0].replacement); + 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/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 1a6788573ec..f78f3150637 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1771,12 +1771,18 @@ export class EditorGroupView extends Themable implements IEditorGroupView { await this.hostService.focus(getWindow(this.element)); // Let editor handle confirmation if implemented + let handlerDidError = false; if (typeof editor.closeHandler?.confirm === 'function') { - confirmation = await editor.closeHandler.confirm([{ editor, groupId: this.id }]); + try { + confirmation = await editor.closeHandler.confirm([{ editor, groupId: this.id }]); + } catch (e) { + this.logService.error(e); + handlerDidError = true; + } } - // Show a file specific confirmation - else { + // Show a file specific confirmation if there is no handler or it errored + if (typeof editor.closeHandler?.confirm !== 'function' || handlerDidError) { let name: string; if (editor instanceof SideBySideEditorInput) { name = editor.primary.getName(); // prefer shorter names by using primary's name in this case @@ -1839,7 +1845,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private shouldConfirmClose(editor: EditorInput): boolean { if (editor.closeHandler) { - return editor.closeHandler.showConfirm(); // custom handling of confirmation on close + try { + return editor.closeHandler.showConfirm(); // custom handling of confirmation on close + } catch (error) { + this.logService.error(error); + } } return editor.isDirty() && !editor.isSaving(); // editor must be dirty and not saving @@ -1925,7 +1935,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeAllEditors() - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise { + closeAllEditors(options: { excludeConfirming: true }): boolean; + closeAllEditors(options?: ICloseAllEditorsOptions): Promise; + closeAllEditors(options?: ICloseAllEditorsOptions): boolean | Promise { if (this.isEmpty) { // If the group is empty and the request is to close all editors, we still close @@ -1938,22 +1950,21 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return true; } - // Apply the `excludeConfirming` filter if present - let editors = this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options); + // We can go ahead and close "sync" when we exclude confirming editors if (options?.excludeConfirming) { - editors = editors.filter(editor => !this.shouldConfirmClose(editor)); - } - - // Check for confirmation and veto - const veto = await this.handleCloseConfirmation(editors); - if (veto) { - return false; + this.doCloseAllEditors(options); + return true; } - // Do close - this.doCloseAllEditors(options); + // Otherwise go through potential confirmation "async" + return this.handleCloseConfirmation(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)).then(veto => { + if (veto) { + return false; + } - return true; + this.doCloseAllEditors(options); + return true; + }); } private doCloseAllEditors(options?: ICloseAllEditorsOptions): void { diff --git a/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index b4ac414676a..e3b1dd55c69 100644 --- a/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/code/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -39,8 +39,8 @@ const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggle const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: { web: ContextKeyExpr.equals('terminalProcessSupported', true) } }; const openSettings: WatermarkEntry = { text: localize('watermark.openSettings', "Open Settings"), id: 'workbench.action.openSettings' }; -const showCopilot = ContextKeyExpr.or(ContextKeyExpr.equals('chatSetupHidden', false), ContextKeyExpr.equals('chatSetupInstalled', true)); -const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: showCopilot, web: showCopilot } }; +const showChat = ContextKeyExpr.and(ContextKeyExpr.equals('chatSetupHidden', false), ContextKeyExpr.equals('chatSetupDisabled', false)); +const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: showChat, web: showChat } }; const emptyWindowEntries: WatermarkEntry[] = coalesce([ showCommands, diff --git a/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index 2025ec3ea7d..79a7fd74d33 100644 --- a/code/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/code/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/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index f12d6b925df..033a0026ebd 100644 --- a/code/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/code/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/code/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 3a02bb87193..cc5e523f5b8 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -7,7 +7,7 @@ import './media/notificationsToasts.css'; import { localize } from '../../../../nls.js'; import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemContentChangeKind } from '../../../common/notifications.js'; import { IDisposable, dispose, toDisposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { addDisposableListener, EventType, Dimension, scheduleAtNextAnimationFrame, isAncestorOfActiveElement, getWindow, $ } from '../../../../base/browser/dom.js'; +import { addDisposableListener, EventType, Dimension, scheduleAtNextAnimationFrame, isAncestorOfActiveElement, getWindow, $, isElementInBottomRightQuarter, isHTMLElement, isEditableElement, getActiveElement } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { NotificationsList } from './notificationsList.js'; import { Event, Emitter } from '../../../../base/common/event.js'; @@ -141,6 +141,13 @@ export class NotificationsToasts extends Themable implements INotificationsToast return; // do not show toasts for silenced notifications } + if (item.priority === NotificationPriority.OPTIONAL) { + const activeElement = getActiveElement(); + if (isHTMLElement(activeElement) && isEditableElement(activeElement) && isElementInBottomRightQuarter(activeElement, this.layoutService.mainContainer)) { + return; // skip showing optional toast that potentially covers input fields + } + } + // Optimization: it is possible that a lot of notifications are being // added in a very short time. To prevent this kind of spam, we protect // against showing too many notifications at once. Since they can always diff --git a/code/src/vs/workbench/browser/parts/paneCompositePart.ts b/code/src/vs/workbench/browser/parts/paneCompositePart.ts index d1d67e1ebef..f164e31785e 100644 --- a/code/src/vs/workbench/browser/parts/paneCompositePart.ts +++ b/code/src/vs/workbench/browser/parts/paneCompositePart.ts @@ -40,6 +40,7 @@ import { ViewsSubMenu } from './views/viewPaneContainer.js'; import { getActionBarActions } from '../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IHoverService } from '../../../platform/hover/browser/hover.js'; import { HiddenItemStrategy, WorkbenchToolBar } from '../../../platform/actions/browser/toolbar.js'; +import { DeferredPromise } from '../../../base/common/async.js'; export enum CompositeBarPosition { TOP, @@ -130,7 +131,7 @@ export abstract class AbstractPaneCompositePart extends CompositePart | undefined = undefined; protected contentDimension: Dimension | undefined; constructor( @@ -511,21 +512,34 @@ export abstract class AbstractPaneCompositePart extends CompositePart { if (this.blockOpening) { - return undefined; // Workaround against a potential race condition + // Workaround against a potential race condition when calling + // `setPartHidden` we may end up in `openPaneComposite` again. + // But we still want to return the result of the original call, + // so we return the promise of the original call. + return this.blockOpening.p; } + let blockOpening: DeferredPromise | undefined; if (!this.layoutService.isVisible(this.partId)) { try { - this.blockOpening = true; + blockOpening = this.blockOpening = new DeferredPromise(); this.layoutService.setPartHidden(false, this.partId); } finally { - this.blockOpening = false; + this.blockOpening = undefined; } } - return this.openComposite(id, focus) as PaneComposite; + try { + const result = this.openComposite(id, focus) as PaneComposite | undefined; + blockOpening?.complete(result); + + return result; + } catch (error) { + blockOpening?.error(error); + throw error; + } } getPaneComposite(id: string): PaneCompositeDescriptor | undefined { diff --git a/code/src/vs/workbench/browser/parts/panel/panelActions.ts b/code/src/vs/workbench/browser/parts/panel/panelActions.ts index 6e43176a62e..bff8b9abf9e 100644 --- a/code/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/code/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -9,7 +9,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { MenuId, MenuRegistry, registerAction2, Action2, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { isHorizontal, IWorkbenchLayoutService, PanelAlignment, Parts, Position, positionToString } from '../../../services/layout/browser/layoutService.js'; -import { PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from '../../../common/contextkeys.js'; +import { IsAuxiliaryTitleBarContext, PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from '../../../common/contextkeys.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; @@ -323,7 +323,14 @@ MenuRegistry.appendMenuItems([ icon: panelOffIcon, toggled: { condition: PanelVisibleContext, icon: panelIcon } }, - when: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), + when: + ContextKeyExpr.and( + IsAuxiliaryTitleBarContext.negate(), + ContextKeyExpr.or( + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), + ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both') + ) + ), order: 1 } } diff --git a/code/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/code/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 9e1e2041ab8..a21f003405a 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/code/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -17,7 +17,19 @@ transition: background-color 0.15s ease-out; } -.monaco-workbench .part.statusbar.status-border-top::after { +.monaco-workbench.mac:not(.fullscreen) .part.statusbar:focus { + /* Rounded corners to make focus outline appear properly (unless fullscreen) */ + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} +.monaco-workbench.mac:not(.fullscreen).macos-bigsur-or-newer .part.statusbar:focus { + /* macOS Big Sur increased rounded corners size */ + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; +} + +.monaco-workbench .part.statusbar:not(:focus).status-border-top::after { + /* Top border only visible unless focused to make room for focus outline */ content: ''; position: absolute; top: 0; @@ -73,32 +85,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,27 +100,12 @@ padding-left: 0; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-background-color > .statusbar-item-label { - margin-right: 0; - margin-left: 0; - padding-left: 10px; - padding-right: 10px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.has-background-color > .statusbar-item-label { - padding-left: 3px; - padding-right: 10px; -} - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right.has-background-color > .statusbar-item-label { - padding-left: 10px; - padding-right: 3px; -} - .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; @@ -138,13 +115,39 @@ } .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; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-left.has-background-color > .statusbar-item-label { + padding-left: 3px; + padding-right: 10px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right.has-background-color > .statusbar-item-label { + padding-left: 10px; + padding-right: 3px; +} + .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover:not(.disabled) { text-decoration: none; color: var(--vscode-statusBarItem-hoverForeground); diff --git a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 2e6ce3bc4a2..e601d324307 100644 --- a/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/code/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -188,6 +188,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { persistence: { hideOnKeyDown: true, sticky: focus + }, + appearance: { + maxHeightRatio: 0.9 } } ))); @@ -675,7 +678,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/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts b/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts index e09172e4a7f..f8e5e74bf6b 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/titlebarActions.ts @@ -12,16 +12,14 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activity.js'; import { IAction } from '../../../../base/common/actions.js'; -import { IsAuxiliaryWindowFocusedContext, IsMainWindowFullscreenContext, TitleBarStyleContext, TitleBarVisibleContext } from '../../../common/contextkeys.js'; +import { IsMainWindowFullscreenContext, IsCompactTitleBarContext, 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 --- // export class ToggleTitleBarConfigAction extends Action2 { - constructor(private readonly section: string, title: string, description: string | ILocalizedString | undefined, order: number, mainWindowOnly: boolean, when?: ContextKeyExpression) { - when = ContextKeyExpr.and(mainWindowOnly ? IsAuxiliaryWindowFocusedContext.toNegated() : ContextKeyExpr.true(), when); + constructor(private readonly section: string, title: string, description: string | ILocalizedString | undefined, order: number, when?: ContextKeyExpression) { super({ id: `toggle.${section}`, @@ -54,19 +52,19 @@ export class ToggleTitleBarConfigAction extends Action2 { registerAction2(class ToggleCommandCenter extends ToggleTitleBarConfigAction { constructor() { - super(LayoutSettings.COMMAND_CENTER, localize('toggle.commandCenter', 'Command Center'), localize('toggle.commandCenterDescription', "Toggle visibility of the Command Center in title bar"), 1, false); + super(LayoutSettings.COMMAND_CENTER, localize('toggle.commandCenter', 'Command Center'), localize('toggle.commandCenterDescription', "Toggle visibility of the Command Center in title bar"), 1, IsCompactTitleBarContext.toNegated()); } }); registerAction2(class ToggleNavigationControl extends ToggleTitleBarConfigAction { constructor() { - super('workbench.navigationControl.enabled', localize('toggle.navigation', 'Navigation Controls'), localize('toggle.navigationDescription', "Toggle visibility of the Navigation Controls in title bar"), 2, false, ContextKeyExpr.has('config.window.commandCenter')); + super('workbench.navigationControl.enabled', localize('toggle.navigation', 'Navigation Controls'), localize('toggle.navigationDescription', "Toggle visibility of the Navigation Controls in title bar"), 2, ContextKeyExpr.and(IsCompactTitleBarContext.toNegated(), ContextKeyExpr.has('config.window.commandCenter'))); } }); registerAction2(class ToggleLayoutControl extends ToggleTitleBarConfigAction { constructor() { - super(LayoutSettings.LAYOUT_ACTIONS, localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 4, true); + super(LayoutSettings.LAYOUT_ACTIONS, localize('toggle.layout', 'Layout Controls'), localize('toggle.layoutDescription', "Toggle visibility of the Layout Controls in title bar"), 4); } }); @@ -259,26 +257,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/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 69bf33c0a9c..9fbafc65359 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -26,7 +26,7 @@ import { IStorageService, StorageScope } from '../../../../platform/storage/comm import { Parts, IWorkbenchLayoutService, ActivityBarPosition, LayoutSettings, EditorActionsLocation, EditorTabsMode } from '../../../services/layout/browser/layoutService.js'; import { createActionViewItem, fillInActionBarActions as fillInActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { WindowTitle } from './windowTitle.js'; import { CommandCenterControl } from './commandCenterControl.js'; @@ -54,7 +54,7 @@ import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionba import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { safeIntl } from '../../../../base/common/date.js'; -import { TitleBarVisibleContext } from '../../../common/contextkeys.js'; +import { IsAuxiliaryTitleBarContext, IsCompactTitleBarContext, TitleBarVisibleContext } from '../../../common/contextkeys.js'; export interface ITitleVariable { readonly name: string; @@ -248,6 +248,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { //#endregion + protected scopedContextKeyService: IContextKeyService; + protected rootContainer!: HTMLElement; protected windowControlsContainer: HTMLElement | undefined; @@ -269,8 +271,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { private readonly editorActionsChangeDisposable = this._register(new DisposableStore()); private actionToolBarElement!: HTMLElement; - private globalToolbarMenu: IMenu; - private hasGlobalToolbarEntries = false; + private globalToolbarMenu: IMenu | undefined; private layoutToolbarMenu: IMenu | undefined; private readonly globalToolbarMenuDisposables = this._register(new DisposableStore()); @@ -284,7 +285,10 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { private titleBarStyle: TitlebarStyle; private isInactive: boolean = false; + private readonly isAuxiliary: boolean; + private isCompact = false; + private readonly isCompactContextKey: IContextKey; private readonly windowTitle: WindowTitle; @@ -302,7 +306,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { @IThemeService themeService: IThemeService, @IStorageService private readonly storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @@ -311,10 +315,18 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { ) { super(id, { hasTitle: false }, themeService, storageService, layoutService); + this.isAuxiliary = editorGroupsContainer !== 'main'; + + this.scopedContextKeyService = contextKeyService.createScoped(layoutService.getContainer(targetWindow)); + + const isAuxiliaryTitleBarContext = IsAuxiliaryTitleBarContext.bindTo(this.scopedContextKeyService); + isAuxiliaryTitleBarContext.set(this.isAuxiliary); + + this.isCompactContextKey = IsCompactTitleBarContext.bindTo(this.scopedContextKeyService); + this.isCompactContextKey.set(this.isCompact); + this.titleBarStyle = getTitleBarStyle(this.configurationService); - this.globalToolbarMenu = this._register(this.menuService.createMenu(MenuId.TitleBar, this.contextKeyService)); - this.isAuxiliary = editorGroupsContainer !== 'main'; this.editorService = editorService.createScoped(editorGroupsContainer, this._store); this.editorGroupsContainer = editorGroupsContainer === 'main' ? editorGroupService.mainPart : editorGroupsContainer; @@ -384,9 +396,25 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // Command Center if (event.affectsConfiguration(LayoutSettings.COMMAND_CENTER)) { - this.createTitle(); + this.recreateTitle(); + } + } + + private recreateTitle(): void { + this.createTitle(); + + this._onDidChange.fire(undefined); + } - this._onDidChange.fire(undefined); + updateOptions(options: { compact: boolean }): void { + const oldIsCompact = this.isCompact; + this.isCompact = options.compact; + + this.isCompactContextKey.set(this.isCompact); + + if (oldIsCompact !== this.isCompact) { + this.recreateTitle(); + this.createActionToolBarMenus(true); } } @@ -548,9 +576,8 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.title.innerText = this.windowTitle.getHeader() || this.windowTitle.value; this.titleDisposables.add(this.windowTitle.onDidChange(() => { this.title.innerText = this.windowTitle.getHeader() || this.windowTitle.value; - // layout menubar and other renderings in the titlebar if (this.lastLayoutDimensions) { - this.updateLayout(this.lastLayoutDimensions); + this.updateLayout(this.lastLayoutDimensions); // layout menubar and other renderings in the titlebar } })); } @@ -590,12 +617,12 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } private getKeybinding(action: IAction): ResolvedKeybinding | undefined { - const editorPaneAwareContextKeyService = this.editorGroupsContainer.activeGroup?.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; + const editorPaneAwareContextKeyService = this.editorGroupsContainer.activeGroup?.activeEditorPane?.scopedContextKeyService ?? this.scopedContextKeyService; return this.keybindingService.lookupKeybinding(action.id, editorPaneAwareContextKeyService); } - private createActionToolBar() { + private createActionToolBar(): void { // Creates the action tool bar. Depends on the configuration of the title bar menus // Requires to be recreated whenever editor actions enablement changes @@ -610,7 +637,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { overflowBehavior: { maxItems: 9, exempted: [ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID, ...EDITOR_CORE_NAVIGATION_COMMANDS] }, anchorAlignmentProvider: () => AnchorAlignment.RIGHT, telemetrySource: 'titlePart', - highlightToggledItems: this.editorActionsEnabled, // Only show toggled state for editor actions (Layout actions are not shown as toggled) + highlightToggledItems: this.editorActionsEnabled || this.isAuxiliary, // Only show toggled state for editor actions or auxiliary title bars actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options), hoverDelegate: this.hoverDelegate })); @@ -620,9 +647,9 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } } - private createActionToolBarMenus(update: true | { editorActions?: boolean; layoutActions?: boolean; activityActions?: boolean } = true) { + private createActionToolBarMenus(update: true | { editorActions?: boolean; layoutActions?: boolean; globalActions?: boolean; activityActions?: boolean } = true): void { if (update === true) { - update = { editorActions: true, layoutActions: true, activityActions: true }; + update = { editorActions: true, layoutActions: true, globalActions: true, activityActions: true }; } const updateToolBarActions = () => { @@ -644,12 +671,12 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } // --- Global Actions - const globalToolbarActions = this.globalToolbarMenu.getActions(); - this.hasGlobalToolbarEntries = globalToolbarActions.length > 0; - fillInActionBarActions( - globalToolbarActions, - actions - ); + if (this.globalToolbarMenu) { + fillInActionBarActions( + this.globalToolbarMenu.getActions(), + actions + ); + } // --- Layout Actions if (this.layoutToolbarMenu) { @@ -694,7 +721,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.layoutToolbarMenuDisposables.clear(); if (this.layoutControlEnabled) { - this.layoutToolbarMenu = this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService); + this.layoutToolbarMenu = this.menuService.createMenu(MenuId.LayoutControlMenu, this.scopedContextKeyService); this.layoutToolbarMenuDisposables.add(this.layoutToolbarMenu); this.layoutToolbarMenuDisposables.add(this.layoutToolbarMenu.onDidChange(() => updateToolBarActions())); @@ -703,8 +730,18 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } } - this.globalToolbarMenuDisposables.clear(); - this.globalToolbarMenuDisposables.add(this.globalToolbarMenu.onDidChange(() => updateToolBarActions())); + if (update.globalActions) { + this.globalToolbarMenuDisposables.clear(); + + if (this.globalActionsEnabled) { + this.globalToolbarMenu = this.menuService.createMenu(MenuId.TitleBar, this.scopedContextKeyService); + + this.globalToolbarMenuDisposables.add(this.globalToolbarMenu); + this.globalToolbarMenuDisposables.add(this.globalToolbarMenu.onDidChange(() => updateToolBarActions())); + } else { + this.globalToolbarMenu = undefined; + } + } if (update.activityActions) { this.activityToolbarDisposables.clear(); @@ -761,7 +798,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.contextMenuService.showContextMenu({ getAnchor: () => event, menuId, - contextKeyService: this.contextKeyService, + contextKeyService: this.scopedContextKeyService, domForShadowRoot: isMacintosh && isNative ? event.target : undefined }); } @@ -775,15 +812,15 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { } private get layoutControlEnabled(): boolean { - return !this.isAuxiliary && this.configurationService.getValue(LayoutSettings.LAYOUT_ACTIONS) !== false; + return this.configurationService.getValue(LayoutSettings.LAYOUT_ACTIONS) !== false; } protected get isCommandCenterVisible() { - return this.configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== false; + return !this.isCompact && this.configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== false; } private get editorActionsEnabled(): boolean { - return this.editorGroupService.partOptions.editorActionsLocation === EditorActionsLocation.TITLEBAR || + return !this.isCompact && this.editorGroupService.partOptions.editorActionsLocation === EditorActionsLocation.TITLEBAR || ( this.editorGroupService.partOptions.editorActionsLocation === EditorActionsLocation.DEFAULT && this.editorGroupService.partOptions.showTabs === EditorTabsMode.NONE @@ -792,13 +829,17 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { private get activityActionsEnabled(): boolean { const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); - return !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM); + return !this.isCompact && !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM); + } + + private get globalActionsEnabled(): boolean { + return !this.isCompact; } get hasZoomableElements(): boolean { const hasMenubar = !(this.currentMenubarVisibility === 'hidden' || this.currentMenubarVisibility === 'compact' || (!isWeb && isMacintosh)); const hasCommandCenter = this.isCommandCenterVisible; - const hasToolBarActions = this.hasGlobalToolbarEntries || this.layoutControlEnabled || this.editorActionsEnabled || this.activityActionsEnabled; + const hasToolBarActions = this.globalActionsEnabled || this.layoutControlEnabled || this.editorActionsEnabled || this.activityActionsEnabled; return hasMenubar || hasCommandCenter || hasToolBarActions; } @@ -877,6 +918,8 @@ export class MainBrowserTitlebarPart extends BrowserTitlebarPart { export interface IAuxiliaryTitlebarPart extends ITitlebarPart, IView { readonly container: HTMLElement; readonly height: number; + + updateOptions(options: { compact: boolean }): void; } export class AuxiliaryBrowserTitlebarPart extends BrowserTitlebarPart implements IAuxiliaryTitlebarPart { diff --git a/code/src/vs/workbench/browser/parts/views/treeView.ts b/code/src/vs/workbench/browser/parts/views/treeView.ts index 9a152cef023..bc660c6f7a1 100644 --- a/code/src/vs/workbench/browser/parts/views/treeView.ts +++ b/code/src/vs/workbench/browser/parts/views/treeView.ts @@ -460,6 +460,9 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { set title(name: string) { this._title = name; + if (this.tree) { + this.tree.ariaLabel = this._title; + } this._onDidChangeTitle.fire(this._title); } @@ -830,6 +833,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return command; } + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; diff --git a/code/src/vs/workbench/browser/web.api.ts b/code/src/vs/workbench/browser/web.api.ts index 9ae723f51fd..24f54228122 100644 --- a/code/src/vs/workbench/browser/web.api.ts +++ b/code/src/vs/workbench/browser/web.api.ts @@ -73,10 +73,10 @@ export interface IWorkbench { retrievePerformanceMarks(): Promise<[string, readonly PerformanceMark[]][]>; /** - * Allows to open a `URI` with the standard opener service of the + * Allows to open a target Uri with the standard opener service of the * workbench. */ - openUri(target: URI): Promise; + openUri(target: URI | UriComponents): Promise; }; window: { @@ -357,11 +357,6 @@ export interface IWorkbenchConstructionOptions { */ readonly initialColorTheme?: IInitialColorTheme; - /** - * Welcome dialog. Can be dismissed by the user. - */ - readonly welcomeDialog?: IWelcomeDialog; - //#endregion @@ -657,40 +652,6 @@ export interface IInitialColorTheme { readonly colors?: { [colorId: string]: string }; } -export interface IWelcomeDialog { - - /** - * Unique identifier of the welcome dialog. The identifier will be used to determine - * if the dialog has been previously displayed. - */ - id: string; - - /** - * Title of the welcome dialog. - */ - title: string; - - /** - * Button text of the welcome dialog. - */ - buttonText: string; - - /** - * Button command to execute from the welcome dialog. - */ - buttonCommand: string; - - /** - * Message text for the welcome dialog. - */ - message: string; - - /** - * Media to include in the welcome dialog. - */ - media: { altText: string; path: string }; -} - export interface IDefaultView { /** @@ -794,6 +755,7 @@ export interface ISettingsSyncOptions { * Authentication provider */ readonly authenticationProvider?: { + /** * Unique identifier of the authentication provider. */ @@ -840,6 +802,7 @@ export interface IDevelopmentOptions { * when remote resolvers are used in the web. */ export interface IRemoteResourceProvider { + /** * Path the workbench should delegate requests to. The embedder should * install a service worker on this path and emit {@link onDidReceiveRequest} @@ -858,6 +821,7 @@ export interface IRemoteResourceProvider { * headers, but for now we only deal with GET requests. */ export interface IRemoteResourceRequest { + /** * Request URI. Generally will begin with the current * origin and {@link IRemoteResourceProvider.pathPrefix}. diff --git a/code/src/vs/workbench/browser/web.factory.ts b/code/src/vs/workbench/browser/web.factory.ts index 422fbba83e7..0ebe2713def 100644 --- a/code/src/vs/workbench/browser/web.factory.ts +++ b/code/src/vs/workbench/browser/web.factory.ts @@ -5,7 +5,7 @@ import { ITunnel, ITunnelOptions, IWorkbench, IWorkbenchConstructionOptions, Menu } from './web.api.js'; import { BrowserMain } from './web.main.js'; -import { URI } from '../../base/common/uri.js'; +import { URI, UriComponents } from '../../base/common/uri.js'; import { IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { CommandsRegistry } from '../../platform/commands/common/commands.js'; import { mark, PerformanceMark } from '../../base/common/performance.js'; @@ -125,10 +125,10 @@ export namespace env { /** * {@linkcode IWorkbench.env IWorkbench.env.openUri} */ - export async function openUri(target: URI): Promise { + export async function openUri(target: URI | UriComponents): Promise { const workbench = await workbenchPromise.p; - return workbench.env.openUri(target); + return workbench.env.openUri(URI.isUri(target) ? target : URI.from(target)); } } diff --git a/code/src/vs/workbench/browser/web.main.ts b/code/src/vs/workbench/browser/web.main.ts index 5dcdad8aaac..8aec734eb49 100644 --- a/code/src/vs/workbench/browser/web.main.ts +++ b/code/src/vs/workbench/browser/web.main.ts @@ -27,7 +27,7 @@ import { IAnyWorkspaceIdentifier, IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW import { IWorkbenchConfigurationService } from '../services/configuration/common/configuration.js'; import { onUnexpectedError } from '../../base/common/errors.js'; import { setFullscreen } from '../../base/browser/browser.js'; -import { URI } from '../../base/common/uri.js'; +import { URI, UriComponents } from '../../base/common/uri.js'; import { WorkspaceService } from '../services/configuration/browser/configurationService.js'; import { ConfigurationCache } from '../services/configuration/common/configurationCache.js'; import { ISignService } from '../../platform/sign/common/sign.js'; @@ -182,8 +182,8 @@ export class BrowserMain extends Disposable { return timerService.getPerformanceMarks(); }, - async openUri(uri: URI): Promise { - return openerService.open(uri, {}); + async openUri(uri: URI | UriComponents): Promise { + return openerService.open(URI.isUri(uri) ? uri : URI.from(uri), {}); } }, logger: { diff --git a/code/src/vs/workbench/browser/workbench.contribution.ts b/code/src/vs/workbench/browser/workbench.contribution.ts index 6998a37dfdb..cfdfc068994 100644 --- a/code/src/vs/workbench/browser/workbench.contribution.ts +++ b/code/src/vs/workbench/browser/workbench.contribution.ts @@ -533,10 +533,22 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('workbench.panel.opensMaximized.preserve', "Open the panel to the state that it was in, before it was closed.") ] }, + 'workbench.secondarySideBar.defaultVisibility': { + 'type': 'string', + 'enum': ['hidden', 'visibleInWorkspace', 'visible'], + 'default': 'hidden', + 'tags': ['onExp'], + 'description': localize('secondarySideBarDefaultVisibility', "Controls the default visibility of the secondary side bar in workspaces or empty windows opened for the first time."), + 'enumDescriptions': [ + localize('workbench.secondarySideBar.defaultVisibility.hidden', "The secondary side bar is hidden by default."), + localize('workbench.secondarySideBar.defaultVisibility.visibleInWorkspace', "The secondary side bar is visible by default if a workspace is opened."), + localize('workbench.secondarySideBar.defaultVisibility.visible', "The secondary side bar is visible by default.") + ] + }, 'workbench.secondarySideBar.showLabels': { 'type': 'boolean', 'default': true, - 'markdownDescription': localize('secondarySideBarShowLabels', "Controls whether activity items in the secondary sidebar title are shown as label or icon. This setting only has an effect when {0} is not set to {1}.", '`#workbench.activityBar.location#`', '`top`'), + 'markdownDescription': localize('secondarySideBarShowLabels', "Controls whether activity items in the secondary side bar title are shown as label or icon. This setting only has an effect when {0} is not set to {1}.", '`#workbench.activityBar.location#`', '`top`'), }, 'workbench.statusBar.visible': { 'type': 'boolean', @@ -603,10 +615,16 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('settings.editor.ui', "Use the settings UI editor."), localize('settings.editor.json', "Use the JSON file editor."), ], - 'description': localize('settings.editor.desc', "Determines which settings editor to use by default."), + 'description': localize('settings.editor.desc', "Determines which Settings editor to use by default."), 'default': 'ui', 'scope': ConfigurationScope.WINDOW }, + 'workbench.settings.showSuggestions': { + 'type': 'boolean', + 'default': false, + 'description': localize('settings.showSuggestions', "Controls whether setting suggestions are shown below the search bar in the Settings editor."), + 'tags': ['experimental'] + }, 'workbench.hover.delay': { 'type': 'number', 'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."), diff --git a/code/src/vs/workbench/common/contextkeys.ts b/code/src/vs/workbench/common/contextkeys.ts index d11f9b168aa..548fe41ac89 100644 --- a/code/src/vs/workbench/common/contextkeys.ts +++ b/code/src/vs/workbench/common/contextkeys.ts @@ -34,6 +34,7 @@ export const TemporaryWorkspaceContext = new RawContextKey('temporaryWo export const IsMainWindowFullscreenContext = new RawContextKey('isFullscreen', false, localize('isFullscreen', "Whether the main window is in fullscreen mode")); export const IsAuxiliaryWindowFocusedContext = new RawContextKey('isAuxiliaryWindowFocusedContext', false, localize('isAuxiliaryWindowFocusedContext', "Whether an auxiliary window is focused")); +export const IsWindowAlwaysOnTopContext = new RawContextKey('isWindowAlwaysOnTop', false, localize('isWindowAlwaysOnTop', "Whether the window is always on top")); export const HasWebFileSystemAccess = new RawContextKey('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access) @@ -111,6 +112,8 @@ export const StatusBarFocused = new RawContextKey('statusBarFocused', f export const TitleBarStyleContext = new RawContextKey('titleBarStyle', 'custom', localize('titleBarStyle', "Style of the window title bar")); export const TitleBarVisibleContext = new RawContextKey('titleBarVisible', false, localize('titleBarVisible', "Whether the title bar is visible")); +export const IsAuxiliaryTitleBarContext = new RawContextKey('isAuxiliaryTitleBar', false, localize('isAuxiliaryTitleBar', "Title bar is in an auxiliary window")); +export const IsCompactTitleBarContext = new RawContextKey('isCompactTitleBar', false, localize('isCompactTitleBar', "Title bar is in compact mode")); //#endregion diff --git a/code/src/vs/workbench/common/editor.ts b/code/src/vs/workbench/common/editor.ts index a3e8516c6e6..3db16dd0d94 100644 --- a/code/src/vs/workbench/common/editor.ts +++ b/code/src/vs/workbench/common/editor.ts @@ -410,7 +410,7 @@ export interface IFileEditorFactory { typeId: string; /** - * Creates new new editor capable of showing files. + * Creates new editor capable of showing files. */ createFileEditor(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredLanguageId: string | undefined, preferredContents: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; @@ -504,12 +504,12 @@ export interface IResourceSideBySideEditorInput extends IBaseUntypedEditorInput /** * The right hand side editor to open inside a side-by-side editor. */ - readonly primary: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly primary: Omit | Omit | Omit; /** * The left hand side editor to open inside a side-by-side editor. */ - readonly secondary: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly secondary: Omit | Omit | Omit; } /** @@ -524,12 +524,25 @@ export interface IResourceDiffEditorInput extends IBaseUntypedEditorInput { /** * The left hand side editor to open inside a diff editor. */ - readonly original: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly original: Omit | Omit | Omit; /** * The right hand side editor to open inside a diff editor. */ - readonly modified: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly modified: Omit | Omit | Omit; +} + +export interface ITextResourceDiffEditorInput extends IBaseTextResourceEditorInput { + + /** + * The left hand side text editor to open inside a diff editor. + */ + readonly original: Omit | Omit; + + /** + * The right hand side text editor to open inside a diff editor. + */ + readonly modified: Omit | Omit; } /** @@ -558,7 +571,7 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { export interface IMultiDiffEditorResource extends IResourceDiffEditorInput { readonly goToFileResource?: URI; } -export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; +export type IResourceMergeEditorInputSide = (Omit | Omit) & { detail?: string }; /** * A resource merge editor input compares multiple editors @@ -582,12 +595,12 @@ export interface IResourceMergeEditorInput extends IBaseUntypedEditorInput { /** * The base common ancestor of the file to merge. */ - readonly base: IResourceEditorInput | ITextResourceEditorInput; + readonly base: Omit | Omit; /** * The resulting output of the merge. */ - readonly result: IResourceEditorInput | ITextResourceEditorInput; + readonly result: Omit | Omit; } export function isResourceEditorInput(editor: unknown): editor is IResourceEditorInput { diff --git a/code/src/vs/workbench/common/notifications.ts b/code/src/vs/workbench/common/notifications.ts index 6a690593672..08861595cb0 100644 --- a/code/src/vs/workbench/common/notifications.ts +++ b/code/src/vs/workbench/common/notifications.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource } from '../../platform/notification/common/notification.js'; +import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource, IStatusHandle } from '../../platform/notification/common/notification.js'; import { toErrorMessage, isErrorWithActions } from '../../base/common/errorMessage.js'; import { Event, Emitter } from '../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; +import { Disposable } from '../../base/common/lifecycle.js'; import { isCancellationError } from '../../base/common/errors.js'; import { Action } from '../../base/common/actions.js'; import { equals } from '../../base/common/arrays.js'; @@ -35,7 +35,7 @@ export interface INotificationsModel { readonly onDidChangeStatusMessage: Event; - showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle; //#endregion } @@ -275,24 +275,23 @@ export class NotificationsModel extends Disposable implements INotificationsMode return item; } - showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable { + showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle { const item = StatusMessageViewItem.create(message, options); if (!item) { - return Disposable.None; + return { close: () => { } }; } - // Remember as current status message and fire events this._statusMessage = item; this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item }); - return toDisposable(() => { - - // Only reset status message if the item is still the one we had remembered - if (this._statusMessage === item) { - this._statusMessage = undefined; - this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); + return { + close: () => { + if (this._statusMessage === item) { + this._statusMessage = undefined; + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); + } } - }); + }; } } @@ -492,7 +491,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } let priority = notification.priority ?? NotificationPriority.DEFAULT; - if (priority === NotificationPriority.DEFAULT && severity !== Severity.Error) { + if ((priority === NotificationPriority.DEFAULT || priority === NotificationPriority.OPTIONAL) && severity !== Severity.Error) { if (filter.global === NotificationsFilter.ERROR) { priority = NotificationPriority.SILENT; // filtered globally } else if (isNotificationSource(notification.source) && filter.sources.get(notification.source.id) === NotificationsFilter.ERROR) { diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 0f1c1bbcdcb..77bc44df9cf 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -290,6 +290,20 @@ const configuration: IConfigurationNode = { } } }, + 'accessibility.signals.nextEditSuggestion': { + ...signalFeatureBase, + 'description': localize('accessibility.signals.nextEditSuggestion', "Plays a signal - sound / audio cue and/or announcement (alert) when there is a next edit suggestion."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.nextEditSuggestion.sound', "Plays a sound when there is a next edit suggestion."), + ...soundFeatureBase, + }, + 'announcement': { + 'description': localize('accessibility.signals.nextEditSuggestion.announcement', "Announces when there is a next edit suggestion."), + ...announcementFeatureBase, + }, + } + }, 'accessibility.signals.lineHasError': { ...signalFeatureBase, 'description': localize('accessibility.signals.lineHasError', "Plays a signal - sound (audio cue) and/or announcement (alert) - when the active line has an error."), diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 6e000778220..21be375a627 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/code/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,11 +22,13 @@ 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 { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -40,14 +43,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 @@ -60,6 +62,7 @@ interface ICodeBlock { endLine: number; code: string; languageId?: string; + chatSessionId: string | undefined; } export class AccessibleView extends Disposable implements ITextModelContentProvider { @@ -108,7 +111,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, @IStorageService private readonly _storageService: IStorageService, @ITextModelService private readonly textModelResolverService: ITextModelService, - @IQuickInputService private readonly _quickInputService: IQuickInputService + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { super(); @@ -130,7 +134,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'); @@ -184,15 +188,29 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys())); this._register(this._editorWidget.onDidChangeCursorPosition(() => { this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount()); - })); - this._register(this._editorWidget.onDidChangeCursorPosition(() => { const cursorPosition = this._editorWidget.getPosition()?.lineNumber; if (this._codeBlocks && cursorPosition !== undefined) { const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined; this._accessibleViewInCodeBlock.set(inCodeBlock); } + this._playDiffSignals(); })); } + + private _playDiffSignals(): void { + const position = this._editorWidget.getPosition(); + const model = this._editorWidget.getModel(); + if (!position || !model) { + return undefined; + } + const lineContent = model.getLineContent(position.lineNumber); + if (lineContent?.startsWith('+')) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted); + } else if (lineContent?.startsWith('-')) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted); + } + } + provideTextContent(resource: URI): Promise | null { return this._getTextModel(resource); } @@ -238,7 +256,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (!codeBlock || codeBlockIndex === undefined) { return; } - return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined }; + return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined, chatSessionId: codeBlock.chatSessionId }; } navigateToCodeBlock(type: 'next' | 'previous'): void { @@ -382,7 +400,7 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); - this._codeBlocks?.push({ startLine, endLine, code, languageId }); + this._codeBlocks?.push({ startLine, endLine, code, languageId, chatSessionId: undefined }); } }); this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a4f65768d43..9fdfa7cf421 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isAncestorOfActiveElement } from '../../../../../base/browser/dom.js'; import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; import { coalesce } from '../../../../../base/common/arrays.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -21,9 +22,8 @@ import { SuggestController } from '../../../../../editor/contrib/suggest/browser import { localize, localize2 } from '../../../../../nls.js'; import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; -import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { Action2, ICommandPaletteOptions, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; @@ -44,16 +44,16 @@ 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'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js'; -import { ChatMode, validateChatMode } from '../../common/constants.js'; +import { ChatConfiguration, ChatMode, modeToString, validateChatMode } from '../../common/constants.js'; import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { ChatViewId, EditsViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js'; +import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; @@ -98,86 +98,142 @@ export interface IChatViewOpenRequestEntry { response: string; } -export const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog'; +const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog'; -export function registerChatActions() { - registerAction2(class OpenChatGlobalAction extends Action2 { +abstract class OpenChatGlobalAction extends Action2 { + constructor(overrides: Pick, private readonly mode?: ChatMode) { + super({ + ...overrides, + icon: Codicon.copilot, + f1: true, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.Setup.hidden.negate(), + }); + } - constructor() { - super({ - id: CHAT_OPEN_ACTION_ID, - title: localize2('openChat', "Open Chat"), - icon: Codicon.copilot, - f1: true, - category: CHAT_CATEGORY, - precondition: ChatContextKeys.Setup.hidden.toNegated(), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, - mac: { - primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI - } - }, - menu: { - id: MenuId.ChatTitleBarMenu, - group: 'a_open', - order: 1 - } - }); - } + override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise { + opts = typeof opts === 'string' ? { query: opts } : opts; - override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise { - opts = typeof opts === 'string' ? { query: opts } : opts; + const chatService = accessor.get(IChatService); + const widgetService = accessor.get(IChatWidgetService); + const toolsService = accessor.get(ILanguageModelToolsService); + const viewsService = accessor.get(IViewsService); + const hostService = accessor.get(IHostService); - const chatService = accessor.get(IChatService); - const toolsService = accessor.get(ILanguageModelToolsService); - const viewsService = accessor.get(IViewsService); - const hostService = accessor.get(IHostService); - const chatWidget = await showChatView(viewsService); - if (!chatWidget) { - return; + let chatWidget = widgetService.lastFocusedWidget; + // When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one. + // Otherwise, open the view. + if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) { + chatWidget = await showChatView(viewsService); + } + + if (!chatWidget) { + return; + } + + const mode = opts?.mode ?? this.mode; + if (mode && validateChatMode(mode)) { + chatWidget.input.setChatMode(mode); + } + if (opts?.previousRequests?.length && chatWidget.viewModel) { + for (const { request, response } of opts.previousRequests) { + chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); } - if (opts?.mode && validateChatMode(opts.mode)) { - chatWidget.input.setChatMode(opts.mode); + } + if (opts?.attachScreenshot) { + const screenshot = await hostService.getScreenshot(); + if (screenshot) { + chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot)); } - if (opts?.previousRequests?.length && chatWidget.viewModel) { - for (const { request, response } of opts.previousRequests) { - chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); - } + } + if (opts?.query) { + if (opts.query.startsWith('@') && (chatWidget.input.currentMode === ChatMode.Agent || chatService.edits2Enabled)) { + chatWidget.input.setChatMode(ChatMode.Ask); } - if (opts?.attachScreenshot) { - const screenshot = await hostService.getScreenshot(); - if (screenshot) { - chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot)); - } + if (opts.isPartialQuery) { + chatWidget.setInput(opts.query); + } else { + await chatWidget.waitForReady(); + chatWidget.acceptInput(opts.query); } - if (opts?.query) { - if (opts.isPartialQuery) { - chatWidget.setInput(opts.query); - } else { - chatWidget.acceptInput(opts.query); + } + if (opts?.toolIds && opts.toolIds.length > 0) { + for (const toolId of opts.toolIds) { + const tool = toolsService.getTool(toolId); + if (tool) { + chatWidget.attachmentModel.addContext({ + id: tool.id, + name: tool.displayName, + fullName: tool.displayName, + value: undefined, + icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined, + kind: 'tool' + }); } } - if (opts?.toolIds && opts.toolIds.length > 0) { - for (const toolId of opts.toolIds) { - const tool = toolsService.getTool(toolId); - if (tool) { - chatWidget.attachmentModel.addContext({ - id: tool.id, - name: tool.displayName, - fullName: tool.displayName, - value: undefined, - icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined, - isTool: true - }); - } + } + + chatWidget.focusInput(); + } +} + +class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction { + constructor() { + super({ + id: CHAT_OPEN_ACTION_ID, + title: localize2('openChat', "Open Chat"), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, + mac: { + primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI } - } + }, + menu: [{ + id: MenuId.ChatTitleBarMenu, + group: 'a_open', + order: 1 + }] + }); + } +} - chatWidget.focusInput(); +export function getOpenChatActionIdForMode(mode: ChatMode): string { + const modeStr = modeToString(mode); + return `workbench.action.chat.open${modeStr}`; +} + +abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction { + constructor(mode: ChatMode, keybinding?: ICommandPaletteOptions['keybinding']) { + super({ + id: getOpenChatActionIdForMode(mode), + title: localize2('openChatMode', "Open Chat ({0})", modeToString(mode)), + keybinding + }, mode); + } +} + +export function registerChatActions() { + registerAction2(PrimaryOpenChatGlobalAction); + registerAction2(class extends ModeOpenChatGlobalAction { + constructor() { super(ChatMode.Ask); } + }); + registerAction2(class extends ModeOpenChatGlobalAction { + constructor() { + super(ChatMode.Agent, { + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, + linux: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI + } + },); } }); + registerAction2(class extends ModeOpenChatGlobalAction { + constructor() { super(ChatMode.Edit); } + }); registerAction2(class ToggleChatAction extends Action2 { constructor() { @@ -194,9 +250,8 @@ export function registerChatActions() { const viewDescriptorService = accessor.get(IViewDescriptorService); const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId); - const editsLocation = viewDescriptorService.getViewLocationById(EditsViewId); - if (viewsService.isViewVisible(ChatViewId) || (chatLocation === editsLocation && viewsService.isViewVisible(EditsViewId))) { + if (viewsService.isViewVisible(ChatViewId)) { this.updatePartVisibility(layoutService, chatLocation, false); } else { this.updatePartVisibility(layoutService, chatLocation, true); @@ -358,7 +413,7 @@ export function registerChatActions() { constructor() { super({ id: `workbench.action.openChat`, - title: localize2('interactiveSession.open', "Open Editor"), + title: localize2('interactiveSession.open', "New Chat Editor"), f1: true, category: CHAT_CATEGORY, precondition: ChatContextKeys.enabled @@ -541,7 +596,8 @@ export function registerChatActions() { precondition: ContextKeyExpr.and( ContextKeyExpr.or( ChatContextKeys.Entitlement.limited, - ChatContextKeys.Entitlement.pro + ChatContextKeys.Entitlement.pro, + ChatContextKeys.Entitlement.proPlus ), nonEnterpriseCopilotUsers ), @@ -599,12 +655,12 @@ export function registerChatActions() { } }); - registerAction2(class ShowLimitReachedDialogAction extends Action2 { + registerAction2(class ShowQuotaExceededDialogAction extends Action2 { constructor() { super({ id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - title: localize('upgradeChat', "Upgrade to Copilot Pro") + title: localize('upgradeChat', "Upgrade Copilot Plan") }); } @@ -614,30 +670,36 @@ export function registerChatActions() { const dialogService = accessor.get(IDialogService); const telemetryService = accessor.get(ITelemetryService); - const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); - let message: string; - const { chatQuotaExceeded, completionsQuotaExceeded } = chatEntitlementService.quotas; + const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; + const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0; if (chatQuotaExceeded && !completionsQuotaExceeded) { - message = localize('chatQuotaExceeded', "You've run out of free chat messages. You still have free code completions available in the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free code completions available."); } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - message = localize('completionsQuotaExceeded', "You've run out of free code completions. You still have free chat messages available in the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat messages available."); } else { - message = localize('chatAndCompletionsQuotaExceeded', "You've reached the limit of the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and code completions quota."); } - const upgradeToPro = localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to additional models"); + if (chatEntitlementService.quotas.resetDate) { + const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); + const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate); + message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.format(quotaResetDate))].join(' '); + } + + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; + const upgradeToPro = limited ? localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to premium models") : undefined; await dialogService.prompt({ type: 'none', - message: localize('copilotFree', "Copilot Limit Reached"), + message: localize('copilotQuotaReached', "Copilot Quota Reached"), cancelButton: { label: localize('dismiss', "Dismiss"), run: () => { /* noop */ } }, buttons: [ { - label: localize('upgradePro', "Upgrade to Copilot Pro"), + label: limited ? localize('upgradePro', "Upgrade to Copilot Pro") : localize('upgradePlan', "Upgrade Copilot Plan"), run: () => { const commandId = 'workbench.action.chat.upgradePlan'; telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' }); @@ -647,10 +709,10 @@ export function registerChatActions() { ], custom: { icon: Codicon.copilotWarningLarge, - markdownDetails: [ + markdownDetails: coalesce([ { markdown: new MarkdownString(message, true) }, - { markdown: new MarkdownString(upgradeToPro, true) } - ] + upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined + ]) } }); } @@ -684,6 +746,10 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { icon: Codicon.copilot, when: ContextKeyExpr.and( ChatContextKeys.supported, + ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate() + ), ContextKeyExpr.has('config.chat.commandCenter.enabled') ), order: 10001 // to the right of command center @@ -697,6 +763,10 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, { icon: Codicon.copilot, when: ContextKeyExpr.and( ChatContextKeys.supported, + ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate() + ), ContextKeyExpr.has('config.chat.commandCenter.enabled'), ContextKeyExpr.has('config.window.commandCenter').negate(), ), @@ -708,7 +778,7 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { super( 'chat.commandCenter.enabled', localize('toggle.chatControl', 'Copilot Controls'), - localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 5, false, + localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 5, ContextKeyExpr.and( ChatContextKeys.supported, ContextKeyExpr.has('config.chat.commandCenter.enabled') @@ -740,7 +810,6 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben @IActionViewItemService actionViewItemService: IActionViewItemService, @IInstantiationService instantiationService: IInstantiationService, @IChatEntitlementService chatEntitlementService: IChatEntitlementService, - @IConfigurationService configurationService: IConfigurationService, ) { super(); @@ -756,31 +825,23 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben }); const chatExtensionInstalled = chatEntitlementService.sentiment === ChatSentiment.Installed; - const chatHidden = chatEntitlementService.sentiment === ChatSentiment.Disabled; - const { chatQuotaExceeded, completionsQuotaExceeded } = chatEntitlementService.quotas; + const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; - const setupFromDialog = configurationService.getValue('chat.setupFromDialog'); + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; let primaryActionId = TOGGLE_CHAT_ACTION_ID; let primaryActionTitle = localize('toggleChat', "Toggle Chat"); let primaryActionIcon = Codicon.copilot; - if (!chatExtensionInstalled && (!setupFromDialog || chatHidden)) { - primaryActionId = CHAT_SETUP_ACTION_ID; - primaryActionTitle = localize('triggerChatSetup', "Use AI Features with Copilot for free..."); - } else if (chatExtensionInstalled && signedOut) { - primaryActionId = setupFromDialog ? CHAT_SETUP_ACTION_ID : TOGGLE_CHAT_ACTION_ID; - primaryActionTitle = localize('signInToChatSetup', "Sign in to use Copilot..."); - primaryActionIcon = Codicon.copilotNotConnected; - } else if (chatExtensionInstalled && (chatQuotaExceeded || completionsQuotaExceeded)) { - primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - if (chatQuotaExceeded && !completionsQuotaExceeded) { - primaryActionTitle = localize('chatQuotaExceededButton', "Monthly chat messages limit reached. Click for details."); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - primaryActionTitle = localize('completionsQuotaExceededButton', "Monthly code completions limit reached. Click for details."); - } else { - primaryActionTitle = localize('chatAndCompletionsQuotaExceededButton', "Copilot Free plan limit reached. Click for details."); + if (chatExtensionInstalled) { + if (signedOut) { + primaryActionId = CHAT_SETUP_ACTION_ID; + primaryActionTitle = localize('signInToChatSetup', "Sign in to use Copilot..."); + primaryActionIcon = Codicon.copilotNotConnected; + } else if (chatQuotaExceeded && limited) { + primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; + primaryActionTitle = localize('chatQuotaExceededButton', "Copilot Free plan chat messages quota reached. Click for details."); + primaryActionIcon = Codicon.copilotWarning; } - primaryActionIcon = Codicon.copilotWarning; } return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, instantiationService.createInstance(MenuItemAction, { id: primaryActionId, @@ -790,8 +851,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben }, Event.any( chatEntitlementService.onDidChangeSentiment, chatEntitlementService.onDidChangeQuotaExceeded, - chatEntitlementService.onDidChangeEntitlement, - Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('chat.setupFromDialog')) + chatEntitlementService.onDidChangeEntitlement )); // Reduces flicker a bit on reload/restart @@ -799,11 +859,6 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben } } -export function getEditsViewId(accessor: ServicesAccessor): string { - const chatService = accessor.get(IChatService); - return chatService.unifiedViewEnabled ? ChatViewId : EditsViewId; -} - /** * Returns whether we can continue clearing/switching chat sessions, false to cancel. */ @@ -827,7 +882,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, @@ -860,7 +915,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/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/chatAttachPromptAction.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/chatAttachPromptAction.ts deleted file mode 100644 index 1b8d7a77188..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/chatAttachPromptAction.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CHAT_CATEGORY } from '../chatActions.js'; -import { localize2 } from '../../../../../../nls.js'; -import { IChatService } from '../../../common/chatService.js'; -import { ChatContextKeys } from '../../../common/chatContextKeys.js'; -import { Action2 } from '../../../../../../platform/actions/common/actions.js'; -import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; -import { IViewsService } from '../../../../../services/views/common/viewsService.js'; -import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; -import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { IQuickInputService } from '../../../../../../platform/quickinput/common/quickInput.js'; -import { ISelectPromptOptions, askToSelectPrompt } from './dialogs/askToSelectPrompt/askToSelectPrompt.js'; - -/** - * Action ID for the `Attach Prompt` action. - */ -export const ATTACH_PROMPT_ACTION_ID = 'workbench.action.chat.attach.prompt'; - -/** - * Options for the {@link AttachPromptAction} action. - */ -export interface IChatAttachPromptActionOptions extends Pick< - ISelectPromptOptions, 'resource' | 'widget' -> { } - -/** - * Action to attach a prompt to a chat widget input. - */ -export class AttachPromptAction extends Action2 { - constructor() { - super({ - id: ATTACH_PROMPT_ACTION_ID, - title: localize2('workbench.action.chat.attach.prompt.label', "Use Prompt"), - f1: false, - precondition: ChatContextKeys.enabled, - category: CHAT_CATEGORY, - }); - } - - public override async run( - accessor: ServicesAccessor, - options: IChatAttachPromptActionOptions, - ): Promise { - const fileService = accessor.get(IFileService); - const chatService = accessor.get(IChatService); - const labelService = accessor.get(ILabelService); - const viewsService = accessor.get(IViewsService); - const openerService = accessor.get(IOpenerService); - const dialogService = accessor.get(IDialogService); - const promptsService = accessor.get(IPromptsService); - const commandService = accessor.get(ICommandService); - const quickInputService = accessor.get(IQuickInputService); - - // find all prompt files in the user workspace - const promptFiles = await promptsService.listPromptFiles(); - - await askToSelectPrompt({ - ...options, - promptFiles, - chatService, - fileService, - viewsService, - labelService, - dialogService, - openerService, - commandService, - quickInputService, - }); - } -} diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/askToSelectPrompt.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/askToSelectPrompt.ts deleted file mode 100644 index e0e0db793f1..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/askToSelectPrompt.ts +++ /dev/null @@ -1,199 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DOCS_OPTION } from './constants.js'; -import { IChatWidget } from '../../../../chat.js'; -import { attachPrompts } from './utils/attachPrompts.js'; -import { handleButtonClick } from './utils/handleButtonClick.js'; -import { URI } from '../../../../../../../../base/common/uri.js'; -import { IChatService } from '../../../../../common/chatService.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; -import { createPromptPickItem } from './utils/createPromptPickItem.js'; -import { createPlaceholderText } from './utils/createPlaceholderText.js'; -import { extUri } from '../../../../../../../../base/common/resources.js'; -import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { IPromptPath } from '../../../../../common/promptSyntax/service/types.js'; -import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; -import { IFileService } from '../../../../../../../../platform/files/common/files.js'; -import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; -import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; -import { IViewsService } from '../../../../../../../services/views/common/viewsService.js'; -import { IDialogService } from '../../../../../../../../platform/dialogs/common/dialogs.js'; -import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Options for the {@link askToSelectPrompt} function. - */ -export interface ISelectPromptOptions { - /** - * Prompt resource `URI` to attach to the chat input, if any. - * If provided the resource will be pre-selected in the prompt picker dialog, - * otherwise the dialog will show the prompts list without any pre-selection. - */ - readonly resource?: URI; - - /** - * Target chat widget reference to attach the prompt to. If not provided, the command - * attaches the prompt to a `chat panel` widget by default (either the last focused, - * or a new one). If the `alt` (`option` on mac) key was pressed when the prompt is - * selected, the `edits` widget is used instead (likewise, either the last focused, - * or a new one). - */ - readonly widget?: IChatWidget; - - /** - * List of prompt files to show in the selection dialog. - */ - readonly promptFiles: readonly IPromptPath[]; - - readonly fileService: IFileService; - readonly chatService: IChatService; - readonly labelService: ILabelService; - readonly viewsService: IViewsService; - readonly openerService: IOpenerService; - readonly dialogService: IDialogService; - readonly commandService: ICommandService; - readonly quickInputService: IQuickInputService; -} - -/** - * Shows the prompt selection dialog to the user that allows to select a prompt file(s). - * - * If {@link ISelectPromptOptions.resource resource} is provided, the dialog will have - * the resource pre-selected in the prompts list. - */ -export const askToSelectPrompt = async ( - options: ISelectPromptOptions, -): Promise => { - const { promptFiles, resource, quickInputService, labelService } = options; - - const fileOptions = promptFiles.map((promptFile) => { - return createPromptPickItem(promptFile, labelService); - }); - - /** - * Add a link to the documentation to the end of prompts list. - */ - fileOptions.push(DOCS_OPTION); - - // if a resource is provided, create an `activeItem` for it to pre-select - // it in the UI, and sort the list so the active item appears at the top - let activeItem: WithUriValue | undefined; - if (resource) { - activeItem = fileOptions.find((file) => { - return extUri.isEqual(file.value, resource); - }); - - // if no item for the `resource` was found, it means that the resource is not - // in the list of prompt files, so add a new item for it; this ensures that - // the currently active prompt file is always available in the selection dialog, - // even if it is not included in the prompts list otherwise(from location setting) - if (!activeItem) { - activeItem = createPromptPickItem({ - uri: resource, - // "user" prompts are always registered in the prompts list, hence it - // should be safe to assume that `resource` is not "user" prompt here - type: 'local', - }, labelService); - fileOptions.push(activeItem); - } - - fileOptions.sort((file1, file2) => { - if (extUri.isEqual(file1.value, resource)) { - return -1; - } - - if (extUri.isEqual(file2.value, resource)) { - return 1; - } - - return 0; - }); - } - - /** - * If still no active item present, fall back to the first item in the list. - * This can happen only if command was invoked not from a focused prompt file - * (hence the `resource` is not provided in the options). - * - * Fixes the two main cases: - * - when no prompt files found it, pre-selects the documentation link - * - when there is only a single prompt file, pre-selects it - */ - if (!activeItem) { - activeItem = fileOptions[0]; - } - - // otherwise show the prompt file selection dialog - const quickPick = quickInputService.createQuickPick>(); - quickPick.activeItems = activeItem ? [activeItem] : []; - quickPick.placeholder = createPlaceholderText(options); - quickPick.canAcceptInBackground = true; - quickPick.matchOnDescription = true; - quickPick.items = fileOptions; - - const { openerService } = options; - return await new Promise(resolve => { - const disposables = new DisposableStore(); - - let lastActiveWidget = options.widget; - - // then the dialog is hidden or disposed for other reason, - // dispose everything and resolve the main promise - disposables.add({ - dispose() { - quickPick.dispose(); - resolve(); - // if something was attached (lastActiveWidget is set), focus on the target chat input - lastActiveWidget?.focusInput(); - }, - }); - - // handle the prompt `accept` event - disposables.add(quickPick.onDidAccept(async (event) => { - const { selectedItems } = quickPick; - - // sanity check to confirm our expectations - assert( - selectedItems.length === 1, - `Only one item can be accepted, got '${selectedItems.length}'.`, - ); - - const selectedOption = selectedItems[0]; - - // whether user selected the docs link option - const docsSelected = (selectedOption === DOCS_OPTION); - - // if documentation item was selected, open its link in a browser - if (docsSelected) { - // note that opening a file in editor also hides(disposes) the dialog - await openerService.open(selectedOption.value); - return; - } - - // otherwise attach the selected prompt to a chat input - lastActiveWidget = await attachPrompts(selectedItems, options, quickPick.keyMods); - - // if user submitted their selection, close the dialog - if (!event.inBackground) { - disposables.dispose(); - } - })); - - // handle the `button click` event on a list item (edit, delete, etc.) - disposables.add(quickPick.onDidTriggerItemButton( - handleButtonClick.bind(null, { quickPick, ...options }), - )); - - // when the dialog is hidden, dispose everything - disposables.add(quickPick.onDidHide( - disposables.dispose.bind(disposables), - )); - - // finally, reveal the dialog - quickPick.show(); - }); -}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/constants.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/constants.ts deleted file mode 100644 index a7b1de1c72d..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/constants.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../nls.js'; -import { URI } from '../../../../../../../../base/common/uri.js'; -import { Codicon } from '../../../../../../../../base/common/codicons.js'; -import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { ThemeIcon } from '../../../../../../../../base/common/themables.js'; -import { DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; -import { isLinux, isWindows } from '../../../../../../../../base/common/platform.js'; -import { IQuickInputButton, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Name of the `"super"` key based on the current OS. - */ -export const SUPER_KEY_NAME = (isWindows || isLinux) ? 'Ctrl' : '⌘'; - -/** - * Name of the `alt`/`options` key based on the current OS. - */ -export const ALT_KEY_NAME = (isWindows || isLinux) ? 'Alt' : '⌥'; - -/** - * A special quick pick item that links to the documentation. - */ -export const DOCS_OPTION: WithUriValue = Object.freeze({ - type: 'item', - label: localize( - 'commands.prompts.use.select-dialog.docs-label', - 'Learn how to create reusable prompts', - ), - description: DOCUMENTATION_URL, - tooltip: DOCUMENTATION_URL, - value: URI.parse(DOCUMENTATION_URL), -}); - -/** - * Button that opens a prompt file in the editor. - */ -export const EDIT_BUTTON: IQuickInputButton = Object.freeze({ - tooltip: localize( - 'commands.prompts.use.select-dialog.open-button.tooltip', - "edit ({0}-key + enter)", - SUPER_KEY_NAME, - ), - iconClass: ThemeIcon.asClassName(Codicon.edit), -}); - -/** - * Button that deletes a prompt file. - */ -export const DELETE_BUTTON: IQuickInputButton = Object.freeze({ - tooltip: localize('delete', "delete"), - iconClass: ThemeIcon.asClassName(Codicon.trash), -}); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/attachPrompts.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/attachPrompts.ts deleted file mode 100644 index 2a1df480d69..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/attachPrompts.ts +++ /dev/null @@ -1,126 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISelectPromptOptions } from '../askToSelectPrompt.js'; -import { IChatWidget, showChatView, showEditsView } from '../../../../../chat.js'; -import { IChatAttachPromptActionOptions } from '../../../chatAttachPromptAction.js'; -import { assertDefined, WithUriValue } from '../../../../../../../../../base/common/types.js'; -import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION } from '../../../../chatClearActions.js'; -import { IKeyMods, IQuickPickItem } from '../../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Attaches provided prompts to a chat input. - */ -export const attachPrompts = async ( - files: readonly WithUriValue[], - options: ISelectPromptOptions, - keyMods: IKeyMods, -): Promise => { - const widget = await getChatWidgetObject(options, keyMods); - - for (const file of files) { - widget - .attachmentModel - .promptInstructions - .add(file.value); - } - - return widget; -}; - -/** - * Gets a chat widget based on the provided {@link IChatAttachPromptActionOptions.widget widget} - * reference. If no widget reference is provided, the function will reveal a `chat panel` by default - * (either a last focused, or a new one), but if the {@link altOption} is set to `true`, a `chat edits` - * panel will be revealed instead (likewise either a last focused, or a new one). - * - * @throws if failed to reveal a chat widget. - */ -const getChatWidgetObject = async ( - options: ISelectPromptOptions, - keyMods: IKeyMods, -): Promise => { - const { widget } = options; - const { alt, ctrlCmd } = keyMods; - - // if `ctrl/cmd` key was pressed, create a new chat session - if (ctrlCmd) { - return await openNewChat(options, alt); - } - - // if no widget reference is present, the command was triggered from outside of - // an active chat input, so we reveal a chat widget window based on the `alt` - // key modifier state when a prompt was selected from the picker UI dialog - if (!widget) { - return await showExistingChat(options, alt); - } - - return widget; -}; - -/** - * Opens a new chat session based on the `unified chat view` mode - * enablement, and provided `edits` flag. - */ -const openNewChat = async ( - options: ISelectPromptOptions, - edits: boolean, -): Promise => { - const { commandService, chatService, viewsService } = options; - - // the `unified chat view` mode does not have a separate `edits` view - // therefore we always open a new default chat session in this mode - if (chatService.unifiedViewEnabled === true) { - await commandService.executeCommand(ACTION_ID_NEW_CHAT); - const widget = await showChatView(viewsService); - - assertDefined( - widget, - 'Chat widget must be defined.', - ); - - return widget; - } - - // in non-unified chat view mode, we can open either an `edits` view - // or an `ask` chat view based on the `edits` flag - (edits === true) - ? await commandService.executeCommand(ACTION_ID_NEW_EDIT_SESSION) - : await commandService.executeCommand(ACTION_ID_NEW_CHAT); - - const widget = (edits === true) - ? await showEditsView(viewsService) - : await showChatView(viewsService); - - assertDefined( - widget, - 'Chat widget must be defined.', - ); - - return widget; -}; - -/** - * Shows an existing chat view based on the `unified chat view` mode - * enablement, and provided `edits` flag. - */ -const showExistingChat = async ( - options: ISelectPromptOptions, - edits: boolean, -): Promise => { - const { chatService, viewsService } = options; - - // there is no "edits" view when in the unified view mode - const widget = (edits && (chatService.unifiedViewEnabled === false)) - ? await showEditsView(viewsService) - : await showChatView(viewsService); - - assertDefined( - widget, - 'Revealed chat widget must be defined.', - ); - - return widget; -}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts deleted file mode 100644 index fcf0d6e5c10..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../../nls.js'; -import { ALT_KEY_NAME, SUPER_KEY_NAME } from '../constants.js'; -import { ISelectPromptOptions } from '../askToSelectPrompt.js'; - -/** - * Creates a placeholder text to show in the prompt selection dialog. - */ -export const createPlaceholderText = ( - options: ISelectPromptOptions, -): string => { - const { widget, chatService } = options; - - let text = localize( - 'commands.prompts.use.select-dialog.placeholder', - 'Select a prompt to use', - ); - - // if no widget reference is provided, add the note about `options` - // and `cmd` modifiers users can leverage to alter the command behavior - if (widget === undefined) { - const superModifierNote = localize( - 'commands.prompts.use.select-dialog.super-modifier-note', - '{0}-key to use in new chat', - SUPER_KEY_NAME, - ); - - const altOptionModifierNote = localize( - 'commands.prompts.use.select-dialog.alt-modifier-note', - ' or {0}-key to use in Copilot Edits', - ALT_KEY_NAME, - ); - - // "open in-edits" action does not really fit the unified chat view mode - const openInEditsNote = (chatService.unifiedViewEnabled === true) - ? '' - : altOptionModifierNote; - - text += localize( - 'commands.prompts.use.select-dialog.modifier-notes', - ' (hold {0}{1})', - superModifierNote, - openInEditsNote, - ); - } - - return text; -}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts deleted file mode 100644 index 5203dddcd34..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../../nls.js'; -import { DELETE_BUTTON, EDIT_BUTTON } from '../constants.js'; -import { dirname } from '../../../../../../../../../base/common/resources.js'; -import { WithUriValue } from '../../../../../../../../../base/common/types.js'; -import { IPromptPath } from '../../../../../../common/promptSyntax/service/types.js'; -import { ILabelService } from '../../../../../../../../../platform/label/common/label.js'; -import { getCleanPromptName } from '../../../../../../../../../platform/prompts/common/constants.js'; -import { IQuickPickItem } from '../../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Creates a quick pick item for a prompt. - */ -export const createPromptPickItem = ( - promptFile: IPromptPath, - labelService: ILabelService, -): WithUriValue => { - const { uri, type } = promptFile; - const fileWithoutExtension = getCleanPromptName(uri); - - // if a "user" prompt, don't show its filesystem path in - // the user interface, but do that for all the "local" ones - const description = (type === 'user') - ? localize( - 'user-prompt.capitalized', - 'User prompt', - ) - : labelService.getUriLabel(dirname(uri), { relative: true }); - - const tooltip = (type === 'user') - ? description - : uri.fsPath; - - return { - id: uri.toString(), - type: 'item', - label: fileWithoutExtension, - description, - tooltip, - value: uri, - buttons: [EDIT_BUTTON, DELETE_BUTTON], - }; -}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/handleButtonClick.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/handleButtonClick.ts deleted file mode 100644 index 51768640ad0..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/handleButtonClick.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../../nls.js'; -import { DELETE_BUTTON, EDIT_BUTTON } from '../constants.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { WithUriValue } from '../../../../../../../../../base/common/types.js'; -import { IFileService } from '../../../../../../../../../platform/files/common/files.js'; -import { IOpenerService } from '../../../../../../../../../platform/opener/common/opener.js'; -import { IDialogService } from '../../../../../../../../../platform/dialogs/common/dialogs.js'; -import { getCleanPromptName } from '../../../../../../../../../platform/prompts/common/constants.js'; -import { IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent } from '../../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Options for the {@link handleButtonClick} function. - */ -interface IHandleButtonClickOptions { - quickPick: IQuickPick>; - fileService: IFileService; - openerService: IOpenerService; - dialogService: IDialogService; -} - -/** - * Handler for a button click event on a prompt file item in the prompt selection dialog. - */ -export async function handleButtonClick( - options: IHandleButtonClickOptions, - context: IQuickPickItemButtonEvent>, -) { - const { quickPick, openerService, fileService, dialogService } = options; - const { item, button } = context; - const { value } = item; - - // `edit` button was pressed, open the prompt file in editor - if (button === EDIT_BUTTON) { - return await openerService.open(value); - } - - // `delete` button was pressed, delete the prompt file - if (button === DELETE_BUTTON) { - // sanity check to confirm our expectations - assert( - (quickPick.activeItems.length < 2), - `Expected maximum one active item, got '${quickPick.activeItems.length}'.`, - ); - - const activeItem: WithUriValue | undefined = quickPick.activeItems[0]; - - // sanity checks - prompt file exists and is not a folder - const info = await fileService.stat(value); - assert( - info.isDirectory === false, - `'${value.fsPath}' points to a folder.`, - ); - - // don't close the main prompt selection dialog by the confirmation dialog - const previousIgnoreFocusOut = quickPick.ignoreFocusOut; - quickPick.ignoreFocusOut = true; - - const filename = getCleanPromptName(value); - const { confirmed } = await dialogService.confirm({ - message: localize( - 'commands.prompts.use.select-dialog.delete-prompt.confirm.message', - "Are you sure you want to delete '{0}'?", - filename, - ), - }); - - // restore the previous value of the `ignoreFocusOut` property - quickPick.ignoreFocusOut = previousIgnoreFocusOut; - - // if prompt deletion was not confirmed, nothing to do - if (!confirmed) { - return; - } - - // prompt deletion was confirmed so delete the prompt file - await fileService.del(value); - - // remove the deleted prompt from the selection dialog list - let removedIndex = -1; - quickPick.items = quickPick.items.filter((option, index) => { - if (option === item) { - removedIndex = index; - - return false; - } - - return true; - }); - - // if the deleted item was active item, find a new item to set as active - if (activeItem && (activeItem === item)) { - assert( - removedIndex >= 0, - 'Removed item index must be a valid index.', - ); - - // we set the previous item as new active, or the next item - // if removed prompt item was in the beginning of the list - const newActiveItemIndex = Math.max(removedIndex - 1, 0); - const newActiveItem: WithUriValue | undefined = quickPick.items[newActiveItemIndex]; - - quickPick.activeItems = newActiveItem ? [newActiveItem] : []; - } - - return; - } - - throw new Error(`Unknown button '${JSON.stringify(button)}'.`); -} diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index bb5b6194c8c..562a79ee3e5 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -3,30 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { raceTimeout } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Event } from '../../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize2 } from '../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.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 { ActiveEditorContext } from '../../../../common/contextkeys.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { isChatViewTitleActionContext } from '../../common/chatActions.js'; -import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js'; -import { hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingSession } from '../../common/chatEditingService.js'; -import { IChatService } from '../../common/chatService.js'; -import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; -import { ChatViewId, EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { IChatEditingSession } from '../../common/chatEditingService.js'; +import { ChatMode } from '../../common/constants.js'; +import { ChatViewId, IChatWidget } from '../chat.js'; import { EditingSessionAction } from '../chatEditing/chatEditingActions.js'; -import { ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorContextKeys.js'; import { ChatEditorInput } from '../chatEditorInput.js'; -import { ChatViewPane } from '../chatViewPane.js'; -import { CHAT_CATEGORY, handleCurrentEditingSession, IChatViewOpenOptions } from './chatActions.js'; +import { CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; @@ -74,65 +68,10 @@ export function registerNewChatActions() { } }); - registerAction2(class GlobalClearChatAction extends Action2 { + registerAction2(class NewChatAction extends EditingSessionAction { constructor() { super({ id: ACTION_ID_NEW_CHAT, - title: localize2('chat.newChat.label', "New Chat"), - category: CHAT_CATEGORY, - icon: Codicon.plus, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), ChatContextKeyExprs.unifiedChatEnabled.negate()), - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyL, - mac: { - primary: KeyMod.WinCtrl | KeyCode.KeyL - }, - when: ChatContextKeys.inChatSession - }, - menu: [{ - id: MenuId.ChatContext, - group: 'z_clear', - when: ContextKeyExpr.and( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeys.inUnifiedChat.negate()), - }, - { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeys.inUnifiedChat.negate()), - group: 'navigation', - order: -1 - }] - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - const widgetService = accessor.get(IChatWidgetService); - - let widget = widgetService.lastFocusedWidget; - - if (isChatViewTitleActionContext(context)) { - // Is running in the Chat view title - widget = widgetService.getWidgetBySessionId(context.sessionId); - } - - if (widget) { - announceChatCleared(accessibilitySignalService); - widget.clear(); - widget.focusInput(); - } - } - }); - - registerAction2(class NewEditSessionAction extends EditingSessionAction { - constructor() { - super({ - id: ACTION_ID_NEW_EDIT_SESSION, title: localize2('chat.newEdits.label', "New Chat"), category: CHAT_CATEGORY, icon: Codicon.plus, @@ -144,7 +83,7 @@ export function registerNewChatActions() { }, { id: MenuId.ViewTitle, - when: ChatContextKeyExprs.inEditsOrUnified, + when: ContextKeyExpr.equals('view', ChatViewId), group: 'navigation', order: -1 }], @@ -164,7 +103,6 @@ export function registerNewChatActions() { const context: INewEditSessionActionContext | undefined = args[0]; const accessibilitySignalService = accessor.get(IAccessibilitySignalService); const dialogService = accessor.get(IDialogService); - const chatService = accessor.get(IChatService); if (!(await handleCurrentEditingSession(editingSession, undefined, dialogService))) { return; @@ -174,8 +112,8 @@ export function registerNewChatActions() { await editingSession.stop(); widget.clear(); - await waitForChatSessionCleared(editingSession.chatSessionId, chatService); - widget.attachmentModel.clear(); + await widget.waitForReady(); + widget.attachmentModel.clear(true); widget.input.relatedFiles?.clear(); widget.focusInput(); @@ -196,44 +134,8 @@ export function registerNewChatActions() { } } }); + CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT); - registerAction2(class GlobalEditsDoneAction extends EditingSessionAction { - constructor() { - super({ - id: ChatDoneActionId, - title: localize2('chat.done.label', "Done"), - category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered), - f1: false, - menu: [{ - id: MenuId.ChatEditingWidgetToolbar, - when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey.negate(), hasAppliedChatEditsContextKey, ChatContextKeys.editingParticipantRegistered, ChatContextKeyExprs.inEditsOrUnified), - group: 'navigation', - order: 0 - }] - }); - } - - override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, widget: IChatWidget, ...args: any[]) { - const context = args[0]; - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - if (isChatViewTitleActionContext(context)) { - // Is running in the Chat view title - announceChatCleared(accessibilitySignalService); - if (widget) { - widget.clear(); - widget.attachmentModel.clear(); - widget.focusInput(); - } - } else { - // Is running from f1 or keybinding - announceChatCleared(accessibilitySignalService); - widget.clear(); - widget.attachmentModel.clear(); - widget.focusInput(); - } - } - }); registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction { constructor() { @@ -246,7 +148,7 @@ export function registerNewChatActions() { f1: true, menu: [{ id: MenuId.ViewTitle, - when: ChatContextKeyExprs.inEditsOrUnified, + when: ContextKeyExpr.equals('view', ChatViewId), group: 'navigation', order: -3 }] @@ -269,7 +171,7 @@ export function registerNewChatActions() { f1: true, menu: [{ id: MenuId.ViewTitle, - when: ChatContextKeyExprs.inEditsOrUnified, + when: ContextKeyExpr.equals('view', ChatViewId), group: 'navigation', order: -2 }] @@ -280,93 +182,8 @@ export function registerNewChatActions() { await editingSession.redoInteraction(); } }); - - registerAction2(class GlobalOpenEditsAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.openEditSession', - title: localize2('chat.openEdits.label', "Open {0}", 'Copilot Edits'), - category: CHAT_CATEGORY, - icon: Codicon.goToEditingSession, - f1: true, - precondition: ChatContextKeys.Setup.hidden.toNegated(), - menu: [{ - id: MenuId.ViewTitle, - when: ContextKeyExpr.and( - ContextKeyExpr.equals('view', ChatViewId), - ChatContextKeys.editingParticipantRegistered, - ContextKeyExpr.equals(`view.${EditsViewId}.visible`, false), - ContextKeyExpr.or( - ContextKeyExpr.and(ContextKeyExpr.equals(`workbench.panel.chat.defaultViewContainerLocation`, true), ContextKeyExpr.equals(`workbench.panel.chatEditing.defaultViewContainerLocation`, false)), - ContextKeyExpr.and(ContextKeyExpr.equals(`workbench.panel.chat.defaultViewContainerLocation`, false), ContextKeyExpr.equals(`workbench.panel.chatEditing.defaultViewContainerLocation`, true)), - ), - ChatContextKeys.inUnifiedChat.negate() - ), - group: 'navigation', - order: 1 - }, { - id: MenuId.ChatTitleBarMenu, - group: 'a_open', - order: 2, - when: ChatContextKeyExprs.unifiedChatEnabled.negate() - }, { - id: MenuId.ChatEditingEditorContent, - when: ctxIsGlobalEditingSession, - group: 'navigate', - order: 4, - }], - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI - }, - when: ContextKeyExpr.and(ContextKeyExpr.notEquals('view', EditsViewId), ChatContextKeys.editingParticipantRegistered) - } - }); - } - - async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions) { - opts = typeof opts === 'string' ? { query: opts } : opts; - const viewsService = accessor.get(IViewsService); - const chatView = await viewsService.openView(EditsViewId) - ?? await viewsService.openView(ChatViewId); - - if (!chatView?.widget) { - return; - } - - if (!chatView.widget.viewModel) { - await Event.toPromise( - Event.filter(chatView.widget.onDidChangeViewModel, () => !!chatView.widget.viewModel) - ); - } - - if (opts?.query) { - if (opts.isPartialQuery) { - chatView.widget.setInput(opts.query); - } else { - chatView.widget.acceptInput(opts.query); - } - } - - chatView.widget.focusInput(); - } - }); } function announceChatCleared(accessibilitySignalService: IAccessibilitySignalService): void { accessibilitySignalService.playSignal(AccessibilitySignal.clear); } - -export async function waitForChatSessionCleared(sessionId: string, chatService: IChatService): Promise { - if (!chatService.getSession(sessionId)) { - return; - } - - // The ChatWidget just signals cancellation to its host viewpane or editor. Clearing the session is now async, we need to wait for it to finish. - // This is expected to always happen. - await raceTimeout(Event.toPromise( - Event.filter(chatService.onDidDisposeSession, e => e.sessionId === sessionId), - ), 2000); -} diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 7e6a74fba21..e6f04e259e8 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -538,7 +538,8 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), - codemapperUri: codeBlockInfo.codemapperUri + codemapperUri: codeBlockInfo.codemapperUri, + chatSessionId: codeBlockInfo.chatSessionId, }; } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index a627d291bac..dfb0dbb7638 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -3,24 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { groupBy } from '../../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../../../base/common/keybindings.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { isElectron } from '../../../../../base/common/platform.js'; -import { basename, dirname } from '../../../../../base/common/resources.js'; -import { compare } from '../../../../../base/common/strings.js'; +import { basename, dirname, extUri } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { WithUriValue } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { Command } from '../../../../../editor/common/languages.js'; +import { Command, SymbolKinds } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -30,13 +31,13 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../../services/host/browser/host.js'; import { VIEW_ID as SEARCH_VIEW_ID } from '../../../../services/search/common/search.js'; import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; @@ -50,40 +51,62 @@ import { SearchView } from '../../../search/browser/searchView.js'; import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js'; import { SearchContext } from '../../../search/common/constants.js'; import { IChatAgentService } from '../../common/chatAgents.js'; -import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, OmittedState } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; +import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; -import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js'; +import { createFilesAndFolderQuickPick } from '../contrib/chatDynamicVariables.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; import { resizeImage } from '../imageUtils.js'; -import { COMMAND_ID as USE_PROMPT_COMMAND_ID } from '../promptSyntax/contributions/usePromptCommand.js'; +import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/attachInstructionsCommand.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { ATTACH_PROMPT_ACTION_ID, AttachPromptAction, IChatAttachPromptActionOptions } from './chatAttachPromptAction/chatAttachPromptAction.js'; +import { runAttachInstructionsAction, registerPromptActions } from './promptActions/index.js'; export function registerChatContextActions() { registerAction2(AttachContextAction); registerAction2(AttachFileToChatAction); registerAction2(AttachFolderToChatAction); registerAction2(AttachSelectionToChatAction); - registerAction2(AttachFileToEditingSessionAction); - registerAction2(AttachFolderToEditingSessionAction); - registerAction2(AttachSelectionToEditingSessionAction); registerAction2(AttachSearchResultAction); } /** * We fill the quickpick with these types, and enable some quick access providers */ -type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | - IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | - IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IReusablePromptQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem; +type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IWorkspaceSymbolsQuickPickItem + | IToolsQuickPickItem | IToolQuickPickItem + | IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem + | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem + | IFolderQuickPickItem | IFolderResultQuickPickItem + | IDiagnosticsQuickPickItem | IDiagnosticsQuickPickItemWithFilter; + +function isIAttachmentQuickPickItem(obj: unknown): obj is IAttachmentQuickPickItem { + return ( + typeof obj === 'object' + && obj !== null + && typeof (obj).kind === 'string' + ); +} + +const attachmentsOrdinals: (IAttachmentQuickPickItem['kind'])[] = [ + // bottom-most + 'tools', + 'command', + 'screenshot', + 'image', + 'workspaceSymbol', + 'diagnostic', + 'instructions', + 'related-files', + 'folder', + 'open-editors', + // top-most +]; /** * These are the types that we can get out of the quick pick @@ -105,18 +128,6 @@ function isISymbolQuickPickItem(obj: unknown): obj is ISymbolQuickPickItem { && !!(obj as ISymbolQuickPickItem).symbol); } -function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IFolderResultQuickPickItem).kind === 'folder-search-result'); -} - -function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter { - return ( - typeof obj === 'object' - && (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter'); -} - function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource { return ( typeof obj === 'object' @@ -124,40 +135,11 @@ function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithRe && URI.isUri((obj as IQuickPickItemWithResource).resource)); } -function isIOpenEditorsQuickPickItem(obj: unknown): obj is IOpenEditorsQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IOpenEditorsQuickPickItem).id === 'open-editors'); -} - -function isISearchResultsQuickPickItem(obj: unknown): obj is ISearchResultsQuickPickItem { - return ( - typeof obj === 'object' - && (obj as ISearchResultsQuickPickItem).kind === 'search-results'); -} -function isScreenshotQuickPickItem(obj: unknown): obj is IScreenShotQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IScreenShotQuickPickItem).kind === 'screenshot'); -} - -function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IRelatedFilesQuickPickItem).kind === 'related-files' - ); -} - -/** - * Checks is a provided object is a prompt instructions quick pick item. - */ -function isPromptInstructionsQuickPickItem(obj: unknown): obj is IReusablePromptQuickPickItem { - if (!obj || typeof obj !== 'object') { - return false; - } - - return ('kind' in obj && obj.kind === 'reusable-prompt'); +interface IToolsQuickPickItem extends IQuickPickItem { + kind: 'tools'; + id: string; + label: string; } interface IRelatedFilesQuickPickItem extends IQuickPickItem { @@ -189,7 +171,6 @@ interface ICommandVariableQuickPickItem extends IQuickPickItem { command: Command; name?: string; value: unknown; - icon?: ThemeIcon; } @@ -198,12 +179,12 @@ interface IToolQuickPickItem extends IQuickPickItem { id: string; name?: string; icon?: ThemeIcon; + tool: IToolData; } -interface IQuickAccessQuickPickItem extends IQuickPickItem { - kind: 'quickaccess'; +interface IWorkspaceSymbolsQuickPickItem extends IQuickPickItem { + kind: 'workspaceSymbol'; id: string; - prefix: string; } interface IOpenEditorsQuickPickItem extends IQuickPickItem { @@ -238,19 +219,19 @@ interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem { } /** - * Quick pick item for reusable prompt attachment. + * Quick pick item for instructions attachment. */ -const REUSABLE_PROMPT_PICK_ID = 'reusable-prompt'; -interface IReusablePromptQuickPickItem extends IQuickPickItem { +const INSTRUCTION_PICK_ID = 'instructions'; +interface IInstructionsQuickPickItem extends IQuickPickItem { /** * The ID of the quick pick item. */ - id: typeof REUSABLE_PROMPT_PICK_ID; + id: typeof INSTRUCTION_PICK_ID; /** - * Unique kind identifier of the reusable prompt attachment. + * Unique kind identifier of the instructions attachment. */ - kind: typeof REUSABLE_PROMPT_PICK_ID; + kind: typeof INSTRUCTION_PICK_ID; /** * Keybinding of the command. @@ -305,13 +286,16 @@ class AttachFileToChatAction extends AttachResourceAction { } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); + const viewsService = accessor.get(IViewsService); const files = this.getResources(accessor, ...args); - - if (files.length) { - (await showChatView(accessor.get(IViewsService)))?.focusInput(); + if (!files.length) { + return; + } + const widget = await showChatView(viewsService); + if (widget) { + widget.focusInput(); for (const file of files) { - variablesService.attachContext('file', file, ChatAgentLocation.Panel); + widget.attachmentModel.addFile(file); } } } @@ -331,13 +315,17 @@ class AttachFolderToChatAction extends AttachResourceAction { } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); - const folders = this.getResources(accessor, ...args); + const viewsService = accessor.get(IViewsService); - if (folders.length) { - (await showChatView(accessor.get(IViewsService)))?.focusInput(); + const folders = this.getResources(accessor, ...args); + if (!folders.length) { + return; + } + const widget = await showChatView(viewsService); + if (widget) { + widget.focusInput(); for (const folder of folders) { - variablesService.attachContext('folder', folder, ChatAgentLocation.Panel); + widget.attachmentModel.addFolder(folder); } } } @@ -357,8 +345,14 @@ class AttachSelectionToChatAction extends Action2 { } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); const editorService = accessor.get(IEditorService); + const viewsService = accessor.get(IViewsService); + + const widget = await showChatView(viewsService); + if (!widget) { + return; + } + const [_, matches] = args; // If we have search matches, it means this is coming from the search widget if (matches && matches.length > 0) { @@ -372,7 +366,7 @@ class AttachSelectionToChatAction extends Action2 { if (!range || range.startLineNumber !== context.range.startLineNumber && range.endLineNumber !== context.range.endLineNumber) { uris.set(context.uri, context.range); - variablesService.attachContext('file', context, ChatAgentLocation.Panel); + widget.attachmentModel.addFile(context.uri, context.range); } } } @@ -380,67 +374,31 @@ class AttachSelectionToChatAction extends Action2 { for (const uri of uris) { const [resource, range] = uri; if (!range) { - variablesService.attachContext('file', { uri: resource }, ChatAgentLocation.Panel); + widget.attachmentModel.addFile(resource); } } } else { const activeEditor = editorService.activeTextEditorControl; const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { - const selection = activeEditor?.getSelection(); + if (activeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { + const selection = activeEditor.getSelection(); if (selection) { - (await showChatView(accessor.get(IViewsService)))?.focusInput(); + widget.focusInput(); const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; - variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.Panel); + widget.attachmentModel.addFile(activeUri, range); } } } } } -class AttachFileToEditingSessionAction extends AttachResourceAction { - - static readonly ID = 'workbench.action.edits.attachFile'; - - constructor() { - super({ - id: AttachFileToEditingSessionAction.ID, - title: localize2('workbench.action.edits.attachFile.label', "Add File to {0}", 'Copilot Edits'), - category: CHAT_CATEGORY, - f1: false, - menu: [{ - id: MenuId.SearchContext, - group: 'z_chat', - order: 2, - when: ContextKeyExpr.and( - ChatContextKeys.enabled, - ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext), - ChatContextKeyExprs.unifiedChatEnabled.negate(), - SearchContext.SearchResultHeaderFocused.negate()), - }] - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); - const files = this.getResources(accessor, ...args); - - if (files.length) { - (await showEditsView(accessor.get(IViewsService)))?.focusInput(); - for (const file of files) { - variablesService.attachContext('file', file, ChatAgentLocation.EditingSession); - } - } - } -} - export class AttachSearchResultAction extends Action2 { - static readonly Name = 'searchResults'; - static readonly ID = 'workbench.action.chat.insertSearchResults'; + + private static readonly Name = 'searchResults'; constructor() { super({ - id: AttachSearchResultAction.ID, + id: 'workbench.action.chat.insertSearchResults', title: localize2('chat.insertSearchResults', 'Add Search Results to Chat'), category: CHAT_CATEGORY, f1: false, @@ -454,9 +412,9 @@ export class AttachSearchResultAction extends Action2 { }] }); } - async run(accessor: ServicesAccessor, ...args: any[]) { + async run(accessor: ServicesAccessor) { const logService = accessor.get(ILogService); - const widget = (await showChatView(accessor.get(IViewsService))); + const widget = await showChatView(accessor.get(IViewsService)); if (!widget) { logService.trace('InsertSearchResultAction: no chat view available'); @@ -472,7 +430,7 @@ export class AttachSearchResultAction extends Action2 { } let insertText = `#${AttachSearchResultAction.Name}`; - const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length); + const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length); // check character before the start of the range. If it's not a space, add a space const model = editor.getModel(); if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') { @@ -486,97 +444,26 @@ export class AttachSearchResultAction extends Action2 { } } -class AttachFolderToEditingSessionAction extends AttachResourceAction { - - static readonly ID = 'workbench.action.edits.attachFolder'; - - constructor() { - super({ - id: AttachFolderToEditingSessionAction.ID, - title: localize2('workbench.action.edits.attachFolder.label', "Add Folder to {0}", 'Copilot Edits'), - category: CHAT_CATEGORY, - f1: false, - precondition: ContextKeyExpr.and( - ChatContextKeys.enabled, - ChatContextKeyExprs.unifiedChatEnabled.negate()), - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); - const folders = this.getResources(accessor, ...args); - - if (folders.length) { - (await showEditsView(accessor.get(IViewsService)))?.focusInput(); - for (const folder of folders) { - variablesService.attachContext('folder', folder, ChatAgentLocation.EditingSession); - } - } - } -} - -class AttachSelectionToEditingSessionAction extends Action2 { - - static readonly ID = 'workbench.action.edits.attachSelection'; +export class AttachContextAction extends Action2 { constructor() { super({ - id: AttachSelectionToEditingSessionAction.ID, - title: localize2('workbench.action.edits.attachSelection.label', "Add Selection to {0}", 'Copilot Edits'), + id: 'workbench.action.chat.attachContext', + title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."), + icon: Codicon.attach, category: CHAT_CATEGORY, - f1: false, - precondition: ContextKeyExpr.and( - ChatContextKeys.enabled, - ContextKeyExpr.or(ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID), TextCompareEditorActiveContext), - ChatContextKeyExprs.unifiedChatEnabled.negate() - ) - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IChatVariablesService); - const editorService = accessor.get(IEditorService); - - const activeEditor = editorService.activeTextEditorControl; - const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - if (editorService.activeTextEditorControl && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { - const selection = activeEditor?.getSelection(); - if (selection) { - (await showEditsView(accessor.get(IViewsService)))?.focusInput(); - const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection; - variablesService.attachContext('file', { uri: activeUri, range }, ChatAgentLocation.EditingSession); - } - } - } -} - -export class AttachContextAction extends Action2 { - - static readonly ID = 'workbench.action.chat.attachContext'; - - constructor(desc: Readonly = { - id: AttachContextAction.ID, - title: localize2('workbench.action.chat.attachContext.label.2', "Add Context"), - icon: Codicon.attach, - category: CHAT_CATEGORY, - keybinding: { - when: ContextKeyExpr.and( - ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), - ChatContextKeys.inChatInput, - ChatContextKeyExprs.inNonUnifiedPanel), - primary: KeyMod.CtrlCmd | KeyCode.Slash, - weight: KeybindingWeight.EditorContrib - }, - menu: [ - { - when: ChatContextKeyExprs.inNonUnifiedPanel, + keybinding: { + when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)), + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + }, + menu: { + when: ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), id: MenuId.ChatInputAttachmentToolbar, group: 'navigation', - order: 2 - } - ] - }) { - super(desc); + order: 3 + }, + }); } private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { @@ -589,37 +476,145 @@ export class AttachContextAction extends Action2 { `:${item.range.startLineNumber}`); } - private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + private async _attachContext(accessor: ServicesAccessor, widget: IChatWidget, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + const commandService = accessor.get(ICommandService); + const clipboardService = accessor.get(IClipboardService); + const editorService = accessor.get(IEditorService); + const labelService = accessor.get(ILabelService); + const viewsService = accessor.get(IViewsService); + const chatEditingService = accessor.get(IChatEditingService); + const hostService = accessor.get(IHostService); + const fileService = accessor.get(IFileService); + const textModelService = accessor.get(ITextModelService); + const quickInputService = accessor.get(IQuickInputService); const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { - if (isISymbolQuickPickItem(pick) && pick.symbol) { + + if (isIAttachmentQuickPickItem(pick)) { + if (pick.kind === 'folder-search-result') { + toAttach.push({ + kind: 'directory', + id: pick.id, + value: pick.resource, + name: basename(pick.resource), + }); + } else if (pick.kind === 'diagnostic-filter') { + toAttach.push({ + id: pick.id, + name: pick.label, + value: pick.filter, + kind: 'diagnostic', + icon: pick.icon, + ...pick.filter, + }); + + } else if (pick.kind === 'open-editors') { + for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { + const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; + if (uri) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: uri }), + value: uri, + name: labelService.getUriBasenameLabel(uri), + }); + } + } + } else if (pick.kind === 'search-results') { + const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; + for (const result of searchView.model.searchResult.matches()) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: result.resource }), + value: result.resource, + name: labelService.getUriBasenameLabel(result.resource), + }); + } + } else if (pick.kind === 'related-files') { + // Get all provider results and show them in a second tier picker + const chatSessionId = widget.viewModel?.sessionId; + if (!chatSessionId || !chatEditingService) { + continue; + } + const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); + if (!relatedFiles) { + continue; + } + const attachments = widget.attachmentModel.getAttachmentIDs(); + const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) + .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { + acc.push({ type: 'separator', label: cur.group }); + for (const file of cur.files) { + acc.push({ + type: 'item', + label: labelService.getUriBasenameLabel(file.uri), + description: labelService.getUriLabel(dirname(file.uri), { relative: true }), + value: file.uri, + disabled: attachments.has(this._getFileContextId({ resource: file.uri })), + picked: true + }); + } + return acc; + }, [])); + const selectedFile = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set') }); + if (selectedFile) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: selectedFile.value }), + value: selectedFile.value, + name: selectedFile.label, + omittedState: OmittedState.NotOmitted + }); + } + } else if (pick.kind === 'screenshot') { + const blob = await hostService.getScreenshot(); + if (blob) { + toAttach.push(convertBufferToScreenshotVariable(blob)); + } + } else if (pick.kind === 'command') { + // Dynamic variable with a followup command + const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); + if (!selection) { + // User made no selection, skip this variable + continue; + } + toAttach.push({ + ...pick, + value: pick.value, + name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, + // Apply the original icon with the new name + fullName: selection + }); + } else if (pick.kind === 'tool') { + toAttach.push({ + id: pick.id, + name: pick.tool.displayName, + fullName: pick.tool.displayName, + value: undefined, + icon: pick.icon, + kind: 'tool' + }); + } else if (pick.kind === 'image') { + const fileBuffer = await clipboardService.readImage(); + toAttach.push({ + id: await imageToHash(fileBuffer), + name: localize('pastedImage', 'Pasted Image'), + fullName: localize('pastedImage', 'Pasted Image'), + value: fileBuffer, + kind: 'image', + }); + } + } else if (isISymbolQuickPickItem(pick) && pick.symbol) { // Workspace symbol toAttach.push({ kind: 'symbol', id: this._getFileContextId(pick.symbol.location), value: pick.symbol.location, symbolKind: pick.symbol.kind, + icon: SymbolKinds.toIcon(pick.symbol.kind), fullName: pick.label, name: pick.symbol.name, }); - } else if (isIFolderSearchResultQuickPickItem(pick)) { - const folder = pick.resource; - toAttach.push({ - id: pick.id, - value: folder, - name: basename(folder), - isFile: false, - isDirectory: true, - }); - } else if (isIDiagnosticsQuickPickItemWithFilter(pick)) { - toAttach.push({ - id: pick.id, - name: pick.label, - value: pick.filter, - kind: 'diagnostic', - icon: pick.icon, - ...pick.filter, - }); } else if (isIQuickPickItemWithResource(pick) && pick.resource) { if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { // checks if the file is an image @@ -632,136 +627,35 @@ export class AttachContextAction extends Action2 { name: pick.label, fullName: pick.label, value: resizedImage, - isImage: true + kind: 'image', + references: [{ reference: pick.resource, kind: 'reference' }] }); } } else { - let isOmitted = false; + let omittedState = OmittedState.NotOmitted; try { const createdModel = await textModelService.createModelReference(pick.resource); createdModel.dispose(); } catch { - isOmitted = true; + omittedState = OmittedState.Full; } toAttach.push({ + kind: 'file', id: this._getFileContextId({ resource: pick.resource }), value: pick.resource, name: pick.label, - isFile: true, - isOmitted + omittedState }); } } else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) { toAttach.push({ - range: undefined, + kind: 'generic', id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }), value: { uri: pick.uri, range: pick.range.decoration }, fullName: pick.label, name: pick.symbolName!, }); - } else if (isIOpenEditorsQuickPickItem(pick)) { - for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { - const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; - if (uri) { - toAttach.push({ - id: this._getFileContextId({ resource: uri }), - value: uri, - name: labelService.getUriBasenameLabel(uri), - isFile: true, - }); - } - } - } else if (isISearchResultsQuickPickItem(pick)) { - const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; - for (const result of searchView.model.searchResult.matches()) { - toAttach.push({ - id: this._getFileContextId({ resource: result.resource }), - value: result.resource, - name: labelService.getUriBasenameLabel(result.resource), - isFile: true, - }); - } - } else if (isRelatedFileQuickPickItem(pick)) { - // Get all provider results and show them in a second tier picker - const chatSessionId = widget.viewModel?.sessionId; - if (!chatSessionId || !chatEditingService) { - continue; - } - const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); - if (!relatedFiles) { - continue; - } - const attachments = widget.attachmentModel.getAttachmentIDs(); - const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) - .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { - acc.push({ type: 'separator', label: cur.group }); - for (const file of cur.files) { - acc.push({ - type: 'item', - label: labelService.getUriBasenameLabel(file.uri), - description: labelService.getUriLabel(dirname(file.uri), { relative: true }), - value: file.uri, - disabled: attachments.has(this._getFileContextId({ resource: file.uri })), - picked: true - }); - } - return acc; - }, [])); - const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true }); - for (const file of selectedFiles ?? []) { - toAttach.push({ - id: this._getFileContextId({ resource: file.value }), - value: file.value, - name: file.label, - isFile: true, - isOmitted: false - }); - } - } else if (isScreenshotQuickPickItem(pick)) { - const blob = await hostService.getScreenshot(); - if (blob) { - toAttach.push(convertBufferToScreenshotVariable(blob)); - } - } else if (isPromptInstructionsQuickPickItem(pick)) { - const options: IChatAttachPromptActionOptions = { widget }; - await commandService.executeCommand(ATTACH_PROMPT_ACTION_ID, options); - } else { - // Anything else is an attachment - const attachmentPick = pick as IAttachmentQuickPickItem; - if (attachmentPick.kind === 'command') { - // Dynamic variable with a followup command - const selection = await commandService.executeCommand(attachmentPick.command.id, ...(attachmentPick.command.arguments ?? [])); - if (!selection) { - // User made no selection, skip this variable - continue; - } - toAttach.push({ - ...attachmentPick, - value: attachmentPick.value, - name: `${typeof attachmentPick.value === 'string' && attachmentPick.value.startsWith('#') ? attachmentPick.value.slice(1) : ''}${selection}`, - // Apply the original icon with the new name - fullName: selection - }); - } else if (attachmentPick.kind === 'tool') { - toAttach.push({ - id: attachmentPick.id, - name: attachmentPick.label, - fullName: attachmentPick.label, - value: undefined, - icon: attachmentPick.icon, - isTool: true - }); - } else if (attachmentPick.kind === 'image') { - const fileBuffer = await clipboardService.readImage(); - toAttach.push({ - id: await imageToHash(fileBuffer), - name: localize('pastedImage', 'Pasted Image'), - fullName: localize('pastedImage', 'Pasted Image'), - value: fileBuffer, - isImage: true - }); - } } } @@ -774,33 +668,23 @@ export class AttachContextAction extends Action2 { } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const quickInputService = accessor.get(IQuickInputService); const chatAgentService = accessor.get(IChatAgentService); - const commandService = accessor.get(ICommandService); const widgetService = accessor.get(IChatWidgetService); - const languageModelToolsService = accessor.get(ILanguageModelToolsService); - const quickChatService = accessor.get(IQuickChatService); const clipboardService = accessor.get(IClipboardService); const editorService = accessor.get(IEditorService); - const labelService = accessor.get(ILabelService); const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const hostService = accessor.get(IHostService); - const extensionService = accessor.get(IExtensionService); - const fileService = accessor.get(IFileService); - const textModelService = accessor.get(ITextModelService); const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); + const chatEditingService = accessor.get(IChatEditingService); - const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0]; + const context: { widget?: IChatWidget; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; if (!widget) { return; } - const chatEditingService = widget.location === ChatAgentLocation.EditingSession || widget.isUnifiedPanelWidget ? accessor.get(IChatEditingService) : undefined; const quickPickItems: IAttachmentQuickPickItem[] = []; - if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { + if (widget.input.selectedLanguageModel?.metadata.capabilities?.vision) { const imageData = await clipboardService.readImage(); if (isImage(imageData)) { quickPickItems.push({ @@ -845,42 +729,30 @@ export class AttachContextAction extends Action2 { } } - for (const tool of languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - const item: IToolQuickPickItem = { - kind: 'tool', - label: tool.displayName ?? '', - id: tool.id, - icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? - }; - if (ThemeIcon.isThemeIcon(tool.icon)) { - item.iconClass = ThemeIcon.asClassName(tool.icon); - } else if (tool.icon) { - item.iconPath = tool.icon; - } - - quickPickItems.push(item); - } - } + quickPickItems.push({ + kind: 'tools', + label: localize('chatContext.tools', 'Tools...'), + iconClass: ThemeIcon.asClassName(Codicon.tools), + id: 'tools', + }); quickPickItems.push({ - kind: 'quickaccess', - label: localize('chatContext.symbol', 'Symbol...'), + kind: 'workspaceSymbol', + label: localize('chatContext.symbol', 'Symbols...'), iconClass: ThemeIcon.asClassName(Codicon.symbolField), - prefix: SymbolsQuickAccessProvider.PREFIX, id: 'symbol' }); quickPickItems.push({ kind: 'folder', - label: localize('chatContext.folder', 'Folder...'), + label: localize('chatContext.folder', 'Files & Folders...'), iconClass: ThemeIcon.asClassName(Codicon.folder), id: 'folder', }); quickPickItems.push({ kind: 'diagnostic', - label: localize('chatContext.diagnstic', 'Problem...'), + label: localize('chatContext.diagnstic', 'Problems...'), iconClass: ThemeIcon.asClassName(Codicon.error), id: 'diagnostic' }); @@ -901,65 +773,54 @@ export class AttachContextAction extends Action2 { }); } - if (context?.showFilesOnly) { - if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || widget.attachmentModel.fileAttachments.length > 0)) { - quickPickItems.unshift({ - kind: 'related-files', - id: 'related-files', - label: localize('chatContext.relatedFiles', 'Related Files'), - iconClass: ThemeIcon.asClassName(Codicon.sparkle), - }); - } - if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { - quickPickItems.unshift({ - kind: 'open-editors', - id: 'open-editors', - label: localize('chatContext.editors', 'Open Editors'), - iconClass: ThemeIcon.asClassName(Codicon.files), - }); - } - if (SearchContext.HasSearchResults.getValue(contextKeyService)) { - quickPickItems.unshift({ - kind: 'search-results', - id: 'search-results', - label: localize('chatContext.searchResults', 'Search Results'), - iconClass: ThemeIcon.asClassName(Codicon.search), - }); - } + if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || widget.attachmentModel.fileAttachments.length > 0)) { + quickPickItems.push({ + kind: 'related-files', + id: 'related-files', + label: localize('chatContext.relatedFiles', 'Related Files'), + iconClass: ThemeIcon.asClassName(Codicon.sparkle), + }); + } + if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { + quickPickItems.push({ + kind: 'open-editors', + id: 'open-editors', + label: localize('chatContext.editors', 'Open Editors'), + iconClass: ThemeIcon.asClassName(Codicon.files), + }); + } + if (SearchContext.HasSearchResults.getValue(contextKeyService)) { + quickPickItems.push({ + kind: 'search-results', + id: 'search-results', + label: localize('chatContext.searchResults', 'Search Results'), + iconClass: ThemeIcon.asClassName(Codicon.search), + }); } // if the `reusable prompts` feature is enabled, add // the appropriate attachment type to the list if (widget.attachmentModel.promptInstructions.featureEnabled) { - const keybinding = keybindingService.lookupKeybinding(USE_PROMPT_COMMAND_ID, contextKeyService); + const keybinding = keybindingService.lookupKeybinding(INSTRUCTIONS_COMMAND_ID, contextKeyService); quickPickItems.push({ - id: REUSABLE_PROMPT_PICK_ID, - kind: REUSABLE_PROMPT_PICK_ID, - label: localize('chatContext.attach.prompt.label', 'Prompt...'), + id: INSTRUCTION_PICK_ID, + kind: INSTRUCTION_PICK_ID, + label: localize('chatContext.attach.instructions.label', 'Instructions...'), iconClass: ThemeIcon.asClassName(Codicon.bookmark), keybinding, }); } - function extractTextFromIconLabel(label: string | undefined): string { - if (!label) { - return ''; + quickPickItems.sort((a, b) => { + let result = attachmentsOrdinals.indexOf(b.kind) - attachmentsOrdinals.indexOf(a.kind); + if (result === 0) { + result = a.label.localeCompare(b.label); } - const match = label.match(/\$\([^\)]+\)\s*(.+)/); - return match ? match[1] : label; - } - - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems.sort(function (a, b) { - - if (a.kind === 'open-editors') { return -1; } - if (b.kind === 'open-editors') { return 1; } - - const first = extractTextFromIconLabel(a.label).toUpperCase(); - const second = extractTextFromIconLabel(b.label).toUpperCase(); + return result; + }); - return compare(first, second); - }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', context?.placeholder); + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', context?.placeholder); } private async _showDiagnosticsPick(instantiationService: IInstantiationService, onBackgroundAccept: (item: IChatContextQuickPickItem[]) => void): Promise { @@ -971,48 +832,61 @@ export class AttachContextAction extends Action2 { filter: item, }); - const filter = await instantiationService.invokeFunction(accessor => - createMarkersQuickPick(accessor, 'problem', items => onBackgroundAccept(items.map(convert)))); + const filter = await instantiationService.invokeFunction(createMarkersQuickPick, items => onBackgroundAccept(items.map(convert))); return filter && convert(filter); } - private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, instantiationService: IInstantiationService, query: string = '', placeholder?: string) { + private _show(accessor: ServicesAccessor, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, query: string = '', placeholder?: string) { + const quickInputService = accessor.get(IQuickInputService); + const quickChatService = accessor.get(IQuickChatService); + const editorService = accessor.get(IEditorService); + const commandService = accessor.get(ICommandService); + const instantiationService = accessor.get(IInstantiationService); + const attach = (isBackgroundAccept: boolean, ...items: IChatContextQuickPickItem[]) => { - this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, isBackgroundAccept, ...items); + instantiationService.invokeFunction(this._attachContext.bind(this), widget, isBackgroundAccept, ...items); }; const providerOptions: AnythingQuickAccessProviderRunOptions = { + additionPicks: quickPickItems, handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { let item: IChatContextQuickPickItem | undefined = inputItem; - if ('kind' in item && item.kind === 'folder') { - item = await this._showFolders(instantiationService); - } else if ('kind' in item && item.kind === 'diagnostic') { - item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i)); - } - if (!item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder); - return; - } + if (isIAttachmentQuickPickItem(item)) { - if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, item.prefix, placeholder); - } else { - if (!clipboardService) { + if (item.kind === 'workspaceSymbol') { + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, SymbolsQuickAccessProvider.PREFIX, placeholder); + return; + } else if (item.kind === 'instructions') { + runAttachInstructionsAction(commandService, { widget }); return; } - attach(isBackgroundAccept, item); - if (isQuickChat(widget)) { - quickChatService.open(); + + if (item.kind === 'folder') { + item = await this._showFolders(instantiationService); + } else if (item.kind === 'diagnostic') { + item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i)); + } else if (item.kind === 'tools') { + item = await instantiationService.invokeFunction(showToolsPick, widget); } + if (!item) { + // restart picker when sub-picker didn't return anything + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', placeholder); + return; + } + } + attach(isBackgroundAccept, item); + if (isQuickChat(widget)) { + quickChatService.open(); + } + }, - additionPicks: quickPickItems, filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); - if (isIOpenEditorsQuickPickItem(item)) { + if (isIAttachmentQuickPickItem(item) && item.kind === 'open-editors') { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) { // There is an open editor that hasn't yet been attached to the chat if (editor.resource && !attachedContext.has(this._getFileContextId({ resource: editor.resource }))) { @@ -1059,7 +933,7 @@ export class AttachContextAction extends Action2 { } private async _showFolders(instantiationService: IInstantiationService): Promise { - const folder = await instantiationService.invokeFunction(accessor => createFolderQuickPick(accessor)); + const folder = await instantiationService.invokeFunction(createFilesAndFolderQuickPick); if (!folder) { return undefined; } @@ -1073,35 +947,125 @@ export class AttachContextAction extends Action2 { } } -registerAction2(class AttachFilesAction extends AttachContextAction { - constructor() { - super({ - id: 'workbench.action.chat.editing.attachContext', - title: localize2('workbench.action.chat.editing.attachContext.label', "Add Context to Copilot Edits"), - shortTitle: localize2('workbench.action.chat.editing.attachContext.shortLabel', "Add Context..."), - f1: false, - category: CHAT_CATEGORY, - menu: { - when: ChatContextKeyExprs.inEditsOrUnified, - id: MenuId.ChatInputAttachmentToolbar, - group: 'navigation', - order: 3 - }, - icon: Codicon.attach, - precondition: ChatContextKeyExprs.inEditsOrUnified, - keybinding: { - when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeyExprs.inEditsOrUnified), - primary: KeyMod.CtrlCmd | KeyCode.Slash, - weight: KeybindingWeight.EditorContrib +async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { + const quickInputService = accessor.get(IQuickInputService); + const markerService = accessor.get(IMarkerService); + const labelService = accessor.get(ILabelService); + + const markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); + const grouped = groupBy(markers, (a, b) => extUri.compare(a.resource, b.resource)); + + const severities = new Set(); + type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; + const items: (MarkerPickItem | IQuickPickSeparator)[] = []; + + let pickCount = 0; + for (const group of grouped) { + const resource = group[0].resource; + + items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); + for (const marker of group) { + pickCount++; + severities.add(marker.severity); + items.push({ + type: 'item', + resource: marker.resource, + label: marker.message, + description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), + entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), + }); + } + } + + items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); + + const store = new DisposableStore(); + const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); + quickPick.canAcceptInBackground = !onBackgroundAccept; + quickPick.placeholder = localize('pickAProblem', 'Select a problem to attach'); + quickPick.items = items; + + return new Promise(resolve => { + store.add(quickPick.onDidHide(() => resolve(undefined))); + store.add(quickPick.onDidAccept(ev => { + if (ev.inBackground) { + onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); + } else { + resolve(quickPick.selectedItems[0]?.entry); + quickPick.dispose(); } - }); + })); + quickPick.show(); + }).finally(() => store.dispose()); +} + +async function showToolsPick(accessor: ServicesAccessor, widget: IChatWidget): Promise { + + const quickPickService = accessor.get(IQuickInputService); + + + function classify(tool: IToolData) { + if (tool.source.type === 'internal' || tool.source.type === 'extension' && !tool.source.isExternalTool) { + return { ordinal: 1, groupLabel: localize('chatContext.tools.internal', 'Built-In') }; + } else if (tool.source.type === 'mcp') { + return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') }; + } else { + return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') }; + } } - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const context = args[0]; - const attachFilesContext = { ...context, showFilesOnly: true }; - return super.run(accessor, attachFilesContext); + type Pick = IToolQuickPickItem & { ordinal: number; groupLabel: string }; + const items: Pick[] = []; + + for (const tool of widget.input.selectedToolsModel.tools.get()) { + if (!tool.canBeReferencedInPrompt) { + continue; + } + const item: Pick = { + tool, + ...classify(tool), + kind: 'tool', + label: tool.toolReferenceName ?? tool.id, + description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined, + id: tool.id, + }; + // if (ThemeIcon.isThemeIcon(tool.icon)) { + // item.iconClass = ThemeIcon.asClassName(tool.icon); + // } else if (tool.icon) { + // item.iconPath = tool.icon; + // } + items.push(item); } -}); -registerAction2(AttachPromptAction); + items.sort((a, b) => { + let res = a.ordinal - b.ordinal; + if (res === 0) { + res = a.label.localeCompare(b.label); + } + return res; + }); + + let lastGroupLabel: string | undefined; + const picks: (IQuickPickSeparator | Pick)[] = []; + + + for (const item of items) { + if (lastGroupLabel !== item.groupLabel) { + picks.push({ type: 'separator', label: item.groupLabel }); + lastGroupLabel = item.groupLabel; + } + picks.push(item); + } + + const result = await quickPickService.pick(picks, { + placeHolder: localize('chatContext.tools.placeholder', 'Select a tool'), + canPickMany: false + }); + + return result; +} + +/** + * Register all actions related to reusable prompt files. + */ +registerPromptActions(); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 1c09419c4c6..f19d7f56cc3 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -22,7 +22,7 @@ export function registerChatCopyActions() { category: CHAT_CATEGORY, menu: { id: MenuId.ChatContext, - when: ChatContextKeys.responseIsFiltered.toNegated(), + when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', } }); @@ -54,7 +54,7 @@ export function registerChatCopyActions() { category: CHAT_CATEGORY, menu: { id: MenuId.ChatContext, - when: ChatContextKeys.responseIsFiltered.toNegated(), + when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', } }); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 0260214b072..ddd608dd436 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -6,6 +6,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { assertType } from '../../../../../base/common/types.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -15,18 +16,17 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; 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 { ChatContextKeys } from '../../common/chatContextKeys.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'; +import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { EditsViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService, showChatView } from '../chat.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; -import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; -import { ACTION_ID_NEW_CHAT, ChatDoneActionId, waitForChatSessionCleared } from './chatClearActions.js'; +import { ACTION_ID_NEW_CHAT } from './chatClearActions.js'; export interface IVoiceChatExecuteActionContext { readonly disableTimeout?: boolean; @@ -54,13 +54,7 @@ export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; constructor() { - const precondition = ContextKeyExpr.and( - // if the input has prompt instructions attached, allow submitting requests even - // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - whenNotInProgressOrPaused, - ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), - ); + const precondition = ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask); super({ id: ChatSubmitAction.ID, @@ -79,14 +73,14 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecuteSecondary, group: 'group_1', order: 1, - when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask) + when: precondition }, { id: MenuId.ChatExecute, order: 4, when: ContextKeyExpr.and( whenNotInProgressOrPaused, - ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), + precondition, ), group: 'navigation', }, @@ -113,15 +107,12 @@ class ToggleChatModeAction extends Action2 { category: CHAT_CATEGORY, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, - ContextKeyExpr.or( - ChatContextKeys.Editing.hasToolsAgent, - ChatContextKeyExprs.unifiedChatEnabled), ChatContextKeys.requestInProgress.negate()), tooltip: localize('setChatMode', "Set Mode"), keybinding: { when: ContextKeyExpr.and( ChatContextKeys.inChatInput, - ChatContextKeyExprs.inEditsOrUnified), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)), primary: KeyMod.CtrlCmd | KeyCode.Period, weight: KeybindingWeight.EditorContrib }, @@ -129,15 +120,11 @@ class ToggleChatModeAction extends Action2 { { id: MenuId.ChatExecute, order: 1, - // Either in edits with agent mode available, or in unified chat view when: ContextKeyExpr.and( ChatContextKeys.enabled, - ContextKeyExpr.or( - ContextKeyExpr.and( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), - ChatContextKeys.Editing.hasToolsAgent, - ), - ChatContextKeys.inUnifiedChat)), + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), + ChatContextKeys.inQuickChat.negate(), + ), group: 'navigation', }, ] @@ -145,7 +132,6 @@ class ToggleChatModeAction extends Action2 { } async run(accessor: ServicesAccessor, ...args: any[]) { - const chatService = accessor.get(IChatService); const commandService = accessor.get(ICommandService); const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); @@ -159,21 +145,21 @@ class ToggleChatModeAction extends Action2 { const chatSession = context.chatWidget.viewModel?.model; const requestCount = chatSession?.getRequests().length ?? 0; const switchToMode = validateChatMode(arg?.mode) ?? this.getNextMode(context.chatWidget, requestCount, configurationService); - const needToClearEdits = (!chatService.unifiedViewEnabled || (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (context.chatWidget.input.currentMode === ChatMode.Edit || switchToMode === ChatMode.Edit))) && requestCount > 0; + const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (context.chatWidget.input.currentMode === ChatMode.Edit || switchToMode === ChatMode.Edit)) && requestCount > 0; if (switchToMode === context.chatWidget.input.currentMode) { return; } if (needToClearEdits) { - // If not in unified view, or not using edits2 and switching into or out of edit mode, ask to discard the session + // If not using edits2 and switching into or out of edit mode, ask to discard the session const phrase = localize('switchMode.confirmPhrase', "Switching chat modes will end your current edit session."); if (!context.editingSession) { return; } 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; @@ -181,7 +167,7 @@ class ToggleChatModeAction extends Action2 { } else { const confirmation = await dialogService.confirm({ title: localize('agent.newSession', "Start new session?"), - message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to continue?"), + message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to change the chat mode?"), primaryButton: localize('agent.newSession.confirm', "Yes"), type: 'info' }); @@ -194,19 +180,16 @@ class ToggleChatModeAction extends Action2 { context.chatWidget.input.setChatMode(switchToMode); if (needToClearEdits) { - const clearAction = chatService.unifiedViewEnabled ? ACTION_ID_NEW_CHAT : ChatDoneActionId; - await commandService.executeCommand(clearAction); + await commandService.executeCommand(ACTION_ID_NEW_CHAT); } } private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService): ChatMode { - const modes = [ChatMode.Agent]; + const modes = [ChatMode.Ask]; if (configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0) { modes.push(ChatMode.Edit); } - if (chatWidget.location === ChatAgentLocation.Panel) { - modes.push(ChatMode.Ask); - } + modes.push(ChatMode.Agent); const modeIndex = modes.indexOf(chatWidget.input.currentMode); const newMode = modes[(modeIndex + 1) % modes.length]; @@ -237,7 +220,7 @@ export class ToggleRequestPausedAction extends Action2 { when: ContextKeyExpr.and( ChatContextKeys.canRequestBePaused, ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), - ChatContextKeyExprs.inEditsOrUnified, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ContextKeyExpr.or(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.inputHasText.negate()), ), group: 'navigation', @@ -254,9 +237,8 @@ export class ToggleRequestPausedAction extends Action2 { } } -export const ChatSwitchToNextModelActionId = 'workbench.action.chat.switchToNextModel'; -export class SwitchToNextModelAction extends Action2 { - static readonly ID = ChatSwitchToNextModelActionId; +class SwitchToNextModelAction extends Action2 { + static readonly ID = 'workbench.action.chat.switchToNextModel'; constructor() { super({ @@ -264,6 +246,27 @@ export class SwitchToNextModelAction extends Action2 { title: localize2('interactive.switchToNextModel.label', "Switch to Next Model"), category: CHAT_CATEGORY, f1: true, + precondition: ChatContextKeys.enabled, + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + widget?.input.switchToNextModel(); + } +} + +export const ChatOpenModelPickerActionId = 'workbench.action.chat.openModelPicker'; +class OpenModelPickerAction extends Action2 { + static readonly ID = ChatOpenModelPickerActionId; + + constructor() { + super({ + id: OpenModelPickerAction.ID, + title: localize2('interactive.openModelPicker.label', "Open Model Picker"), + category: CHAT_CATEGORY, + f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Period, weight: KeybindingWeight.WorkbenchContrib, @@ -278,7 +281,6 @@ export class SwitchToNextModelAction extends Action2 { ChatContextKeys.languageModelsAreUserSelectable, ContextKeyExpr.or( ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel), - ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.EditingSession), ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Editor), ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Notebook), ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Terminal) @@ -288,10 +290,41 @@ export class SwitchToNextModelAction extends Action2 { }); } + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const widgetService = accessor.get(IChatWidgetService); + let widget = widgetService.lastFocusedWidget; + if (!widget || widget.location === ChatAgentLocation.Panel) { + widget = await showChatView(accessor.get(IViewsService)); + } + if (widget) { + widget.input.openModelPicker(); + } + } +} + +export const ChangeChatModelActionId = 'workbench.action.chat.changeModel'; +class ChangeChatModelAction extends Action2 { + static readonly ID = ChangeChatModelActionId; + + constructor() { + super({ + id: ChangeChatModelAction.ID, + title: localize2('interactive.changeModel.label', "Change Model"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + }); + } + override run(accessor: ServicesAccessor, ...args: any[]): void { + const modelInfo: Pick = args[0]; + // Type check the arg + assertType(typeof modelInfo.vendor === 'string' && typeof modelInfo.id === 'string' && typeof modelInfo.family === 'string'); const widgetService = accessor.get(IChatWidgetService); - const widget = widgetService.lastFocusedWidget; - widget?.input.switchToNextModel(); + const widgets = widgetService.getAllWidgets(); + for (const widget of widgets) { + widget.input.switchModel(modelInfo); + } } } @@ -299,13 +332,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; constructor() { - const precondition = ContextKeyExpr.and( - // if the input has prompt instructions attached, allow submitting requests even - // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - whenNotInProgressOrPaused, - ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask), - ); + const precondition = ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask); super({ id: ChatEditingSessionSubmitAction.ID, @@ -323,7 +350,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { { id: MenuId.ChatExecuteSecondary, group: 'group_1', - when: ContextKeyExpr.and(whenNotInProgressOrPaused, ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask),), + when: ContextKeyExpr.and(whenNotInProgressOrPaused, precondition), order: 1 }, { @@ -334,7 +361,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { ContextKeyExpr.and(ChatContextKeys.isRequestPaused, ChatContextKeys.inputHasText), ChatContextKeys.requestInProgress.negate(), ), - ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask),), + precondition), group: 'navigation', }, ] @@ -349,7 +376,7 @@ class SubmitWithoutDispatchingAction extends Action2 { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ); @@ -392,7 +419,7 @@ export class ChatSubmitWithCodebaseAction extends Action2 { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ); @@ -435,109 +462,18 @@ export class ChatSubmitWithCodebaseAction extends Action2 { fullName: codebaseTool.displayName ?? '', value: undefined, icon: ThemeIcon.isThemeIcon(codebaseTool.icon) ? codebaseTool.icon : undefined, - isTool: true + kind: 'tool' }); widget.acceptInput(); } } -class SendToChatEditingAction extends Action2 { - constructor() { - const precondition = ContextKeyExpr.and( - // if the input has prompt instructions attached, allow submitting requests even - // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - ChatContextKeys.inputHasAgent.negate(), - whenNotInProgressOrPaused, - ChatContextKeyExprs.inNonUnifiedPanel - ); - - super({ - id: 'workbench.action.chat.sendToChatEditing', - title: localize2('chat.sendToChatEditing.label', "Send to Copilot Edits"), - precondition, - category: CHAT_CATEGORY, - f1: false, - menu: { - id: MenuId.ChatExecuteSecondary, - group: 'group_1', - order: 4, - when: ContextKeyExpr.and( - ChatContextKeys.enabled, - ChatContextKeys.editingParticipantRegistered, - ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), - ChatContextKeys.location.notEqualsTo(ChatAgentLocation.Editor), - ChatContextKeyExprs.inNonUnifiedPanel - ) - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter, - when: ContextKeyExpr.and( - ChatContextKeys.enabled, - ChatContextKeys.editingParticipantRegistered, - ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), - ChatContextKeys.location.notEqualsTo(ChatAgentLocation.Editor) - ) - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - if (!accessor.get(IChatAgentService).getDefaultAgent(ChatAgentLocation.EditingSession)) { - return; - } - - const widget = args.length > 0 && args[0].widget ? args[0].widget : accessor.get(IChatWidgetService).lastFocusedWidget; - - const viewsService = accessor.get(IViewsService); - const dialogService = accessor.get(IDialogService); - const { widget: editingWidget } = await viewsService.openView(EditsViewId) as ChatViewPane; - if (!editingWidget.viewModel?.sessionId) { - return; - } - const currentEditingSession = editingWidget.viewModel.model.editingSession; - if (!currentEditingSession) { - return; - } - - const currentEditCount = currentEditingSession?.entries.get().length; - if (currentEditCount) { - const result = await dialogService.confirm({ - title: localize('chat.startEditing.confirmation.title', "Start new editing session?"), - message: currentEditCount === 1 - ? localize('chat.startEditing.confirmation.message.one', "Starting a new editing session will end your current editing session containing {0} file. Do you wish to proceed?", currentEditCount) - : localize('chat.startEditing.confirmation.message.many', "Starting a new editing session will end your current editing session containing {0} files. Do you wish to proceed?", currentEditCount), - type: 'info', - primaryButton: localize('chat.startEditing.confirmation.primaryButton', "Yes") - }); - - if (!result.confirmed) { - return; - } - - await currentEditingSession.stop(true); - editingWidget.clear(); - } - - for (const attachment of widget.attachmentModel.attachments) { - editingWidget.attachmentModel.addContext(attachment); - } - - editingWidget.setInput(widget.getInput()); - widget.setInput(''); - widget.attachmentModel.clear(); - editingWidget.acceptInput(); - editingWidget.focusInput(); - } -} - class SendToNewChatAction extends Action2 { constructor() { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ); @@ -565,16 +501,21 @@ class SendToNewChatAction extends Action2 { const context: IChatExecuteActionContext | undefined = args[0]; const widgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); + const dialogService = accessor.get(IDialogService); const widget = context?.widget ?? widgetService.lastFocusedWidget; if (!widget) { return; } - widget.clear(); - if (widget.viewModel) { - await waitForChatSessionCleared(widget.viewModel.sessionId, chatService); + const editingSession = widget.viewModel?.model.editingSession; + if (editingSession) { + if (!(await handleCurrentEditingSession(editingSession, undefined, dialogService))) { + return; + } } + + widget.clear(); + await widget.waitForReady(); widget.acceptInput(context?.inputValue); } } @@ -626,8 +567,9 @@ export function registerChatExecuteActions() { registerAction2(CancelAction); registerAction2(SendToNewChatAction); registerAction2(ChatSubmitWithCodebaseAction); - registerAction2(SendToChatEditingAction); registerAction2(ToggleChatModeAction); registerAction2(ToggleRequestPausedAction); registerAction2(SwitchToNextModelAction); + registerAction2(OpenModelPickerAction); + registerAction2(ChangeChatModelAction); } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index b7a0a6613f8..e87ed5854b6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -11,12 +11,9 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { IExtensionManagementService, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IDefaultChatAgent } from '../../../../../base/common/product.js'; -import { IViewDescriptorService } from '../../../../common/views.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ensureSideBarChatViewSize, showCopilotView } from '../chat.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { showCopilotView } from '../chat.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { IStatusbarService } from '../../../../services/statusbar/browser/statusbar.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatGettingStarted'; @@ -30,10 +27,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb @IViewsService private readonly viewsService: IViewsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IStatusbarService private readonly statusbarService: IStatusbarService, ) { super(); @@ -74,17 +68,9 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb // Open Copilot view showCopilotView(this.viewsService, this.layoutService); - const setupFromDialog = this.configurationService.getValue('chat.setupFromDialog'); - if (!setupFromDialog) { - ensureSideBarChatViewSize(this.viewDescriptorService, this.layoutService, this.viewsService); - } // Only do this once this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); this.recentlyInstalled = false; - - // Enable Copilot related UI if previously disabled - this.statusbarService.updateEntryVisibility('chat.statusBarEntry', true); - this.configurationService.updateValue('chat.commandCenter.enabled', true); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index c0c33942bd5..804fd736053 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -13,14 +13,12 @@ import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../serv import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ChatViewId, IChatWidgetService } from '../chat.js'; import { ChatEditor, IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { waitForChatSessionCleared } from './chatClearActions.js'; enum MoveToNewLocation { Editor = 'Editor', @@ -99,12 +97,11 @@ export function registerMoveActions() { async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNewLocation, _sessionId?: string) { const widgetService = accessor.get(IChatWidgetService); const editorService = accessor.get(IEditorService); - const chatService = accessor.get(IChatService); const widget = (_sessionId ? widgetService.getWidgetBySessionId(_sessionId) : undefined) ?? widgetService.lastFocusedWidget; if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel) { - await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); + await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true, compact: moveTo === MoveToNewLocation.Window } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); return; } @@ -112,9 +109,9 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew const viewState = widget.getViewState(); widget.clear(); - await waitForChatSessionCleared(sessionId, chatService); + await widget.waitForReady(); - const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState: viewState }; + const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState, compact: moveTo === MoveToNewLocation.Window }; await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); } diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 71bfc203f7d..ca8dd5b0af0 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -4,40 +4,27 @@ *--------------------------------------------------------------------------------------------*/ 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 { ChatContextKeys } from '../../common/chatContextKeys.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 { isResponseVM } from '../../common/chatViewModel.js'; +import { ChatMode } from '../../common/constants.js'; +import { IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful'; @@ -57,12 +44,12 @@ export function registerChatTitleActions() { id: MenuId.ChatMessageFooter, group: 'navigation', order: 1, - when: ContextKeyExpr.and(ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) + when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) }, { id: MENU_INLINE_CHAT_WIDGET_SECONDARY, group: 'navigation', order: 1, - when: ContextKeyExpr.and(ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) + when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) }] }); } @@ -104,12 +91,12 @@ export function registerChatTitleActions() { id: MenuId.ChatMessageFooter, group: 'navigation', order: 2, - when: ContextKeyExpr.and(ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig)) + when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ContextKeyExpr.has(enableFeedbackConfig)) }, { id: MENU_INLINE_CHAT_WIDGET_SECONDARY, group: 'navigation', order: 2, - when: ContextKeyExpr.and(ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) + when: ContextKeyExpr.and(ChatContextKeys.extensionParticipantRegistered, ChatContextKeys.isResponse, ChatContextKeys.responseHasError.negate(), ContextKeyExpr.has(enableFeedbackConfig)) }] }); } @@ -233,7 +220,7 @@ export function registerChatTitleActions() { const itemIndex = chatRequests?.findIndex(request => request.id === item.requestId); const widget = chatWidgetService.getWidgetBySessionId(item.sessionId); const mode = widget?.input.currentMode; - if (chatModel?.initialLocation === ChatAgentLocation.EditingSession || chatModel && (mode === ChatMode.Edit || mode === ChatMode.Agent)) { + if (chatModel && (mode === ChatMode.Edit || mode === ChatMode.Agent)) { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); const currentEditingSession = widget?.viewModel?.model.editingSession; @@ -353,279 +340,6 @@ export function registerChatTitleActions() { } } }); - - - registerAction2(class RemoveAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.remove', - title: localize2('chat.removeRequest.label', "Remove Request"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.x, - precondition: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeyExprs.unifiedChatEnabled.negate()), - keybinding: { - primary: KeyCode.Delete, - mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace, - }, - when: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate()), - weight: KeybindingWeight.WorkbenchContrib, - }, - menu: { - id: MenuId.ChatMessageTitle, - group: 'navigation', - order: 2, - when: ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ChatContextKeys.isRequest, ChatContextKeyExprs.unifiedChatEnabled.negate()) - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - let item: ChatTreeItem | undefined = args[0]; - if (!isRequestVM(item)) { - const chatWidgetService = accessor.get(IChatWidgetService); - const widget = chatWidgetService.lastFocusedWidget; - item = widget?.getFocus(); - } - - if (!item) { - return; - } - - const chatService = accessor.get(IChatService); - const chatModel = chatService.getSession(item.sessionId); - if (chatModel?.initialLocation === ChatAgentLocation.EditingSession) { - return; - } - - const requestId = isRequestVM(item) ? item.id : - isResponseVM(item) ? item.requestId : undefined; - - if (requestId) { - const chatService = accessor.get(IChatService); - chatService.removeRequest(item.sessionId, requestId); - } - } - }); - - 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/code/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index ce4d1e6a6b6..e574f8e143e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -18,12 +18,13 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { AddConfigurationAction } from '../../../mcp/browser/mcpCommands.js'; -import { IMcpService, IMcpServer, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; +import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js'; +import { IMcpServer, IMcpService, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatToolInvocation } from '../../common/chatService.js'; import { isResponseVM } from '../../common/chatViewModel.js'; @@ -94,9 +95,9 @@ export class AttachToolsAction extends Action2 { precondition: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), menu: { when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), - id: MenuId.ChatInputAttachmentToolbar, + id: MenuId.ChatInput, group: 'navigation', - order: 1 + order: 100 }, keybinding: { when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent)), @@ -110,12 +111,13 @@ export class AttachToolsAction extends Action2 { const quickPickService = accessor.get(IQuickInputService); const mcpService = accessor.get(IMcpService); + const mcpRegistry = accessor.get(IMcpRegistry); const toolsService = accessor.get(ILanguageModelToolsService); - const extensionService = accessor.get(IExtensionService); const chatWidgetService = accessor.get(IChatWidgetService); const telemetryService = accessor.get(ITelemetryService); const commandService = accessor.get(ICommandService); const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const editorService = accessor.get(IEditorService); let widget = chatWidgetService.lastFocusedWidget; if (!widget) { @@ -145,6 +147,7 @@ export class AttachToolsAction extends Action2 { type ToolPick = IQuickPickItem & { picked: boolean; tool: IToolData; parent: BucketPick }; type AddPick = IQuickPickItem & { pickable: false; run: () => void }; type MyPick = ToolPick | BucketPick | AddPick; + type ActionableButton = IQuickInputButton & { action: () => void }; const addMcpPick: AddPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(AddConfigurationAction.ID) }; const addExpPick: AddPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') }; @@ -178,39 +181,60 @@ export class AttachToolsAction extends Action2 { continue; } - let bucket: BucketPick; + let bucket: BucketPick | undefined; if (tool.source.type === 'mcp') { const mcpServer = mcpServerByTool.get(tool.id); if (!mcpServer) { continue; } - bucket = toolBuckets.get(mcpServer.definition.id) ?? { - type: 'item', - label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label), - status: localize('mcpstatus', "From {0} ({1})", mcpServer.collection.label, McpConnectionState.toString(mcpServer.connectionState.get())), - ordinal: BucketOrdinal.Mcp, - source: tool.source, - picked: false, - children: [] - }; - toolBuckets.set(mcpServer.definition.id, bucket); - } else if (tool.source.type === 'extension') { - const extensionId = tool.source.extensionId; - const ext = extensionService.extensions.find(value => ExtensionIdentifier.equals(value.identifier, extensionId)); - if (!ext) { - continue; + const key = tool.source.type + mcpServer.definition.id; + bucket = toolBuckets.get(key); + + if (!bucket) { + const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id); + const buttons: ActionableButton[] = []; + if (collection?.presentation?.origin) { + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.settingsGear), + tooltip: localize('configMcpCol', "Configure {0}", collection.label), + action: () => editorService.openEditor({ + resource: collection!.presentation!.origin, + }) + }); + } + if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) { + buttons.push({ + iconClass: ThemeIcon.asClassName(Codicon.warning), + tooltip: localize('mcpShowOutput', "Show Output"), + action: () => mcpServer.showOutput(), + }); + } + + bucket = { + type: 'item', + label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label), + status: localize('mcpstatus', "from {0}", mcpServer.collection.label), + ordinal: BucketOrdinal.Mcp, + source: tool.source, + picked: false, + children: [], + buttons, + }; + toolBuckets.set(key, bucket); } + } else if (tool.source.type === 'extension') { + const key = tool.source.type + ExtensionIdentifier.toKey(tool.source.extensionId); - bucket = toolBuckets.get(ExtensionIdentifier.toKey(extensionId)) ?? { + bucket = toolBuckets.get(key) ?? { type: 'item', - label: ext.displayName ?? ext.name, + label: tool.source.label, ordinal: BucketOrdinal.Extension, picked: false, source: tool.source, children: [] }; - toolBuckets.set(ExtensionIdentifier.toKey(ext.identifier), bucket); + toolBuckets.set(key, bucket); } else if (tool.source.type === 'internal') { bucket = defaultBucket; } else { @@ -243,6 +267,9 @@ export class AttachToolsAction extends Action2 { function isAddPick(obj: any): obj is AddPick { return Boolean((obj as AddPick).run); } + function isActionableButton(obj: IQuickInputButton): obj is ActionableButton { + return typeof (obj as ActionableButton).action === 'function'; + } const store = new DisposableStore(); @@ -311,6 +338,13 @@ export class AttachToolsAction extends Action2 { picker.items = picks; picker.show(); + store.add(picker.onDidTriggerItemButton(e => { + if (isActionableButton(e.button)) { + e.button.action(); + store.dispose(); + } + })); + store.add(picker.onDidChangeSelection(selectedPicks => { if (ignoreEvent) { return; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts index 52bc3680170..8825330db97 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts +++ b/code/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/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index f6d1050cbd5..91581f38c9a 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -35,6 +35,7 @@ import { ICodeBlockActionContext } from '../codeBlockPart.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; export class InsertCodeBlockOperation { constructor( @@ -117,6 +118,7 @@ export class ApplyCodeBlockOperation { @IQuickInputService private readonly quickInputService: IQuickInputService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotebookService private readonly notebookService: INotebookService, ) { } @@ -128,7 +130,7 @@ export class ApplyCodeBlockOperation { return; } - if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri)) { + if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri) && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { // reveal the target file try { const editorPane = await this.editorService.openEditor({ resource: codemapperUri }); @@ -148,8 +150,8 @@ export class ApplyCodeBlockOperation { let result: IComputeEditsResult | undefined = undefined; - if (activeEditorControl) { - result = await this.handleTextEditor(activeEditorControl, context.code); + if (activeEditorControl && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { + result = await this.handleTextEditor(activeEditorControl, context.chatSessionId, context.code); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); if (activeNotebookEditor) { @@ -224,14 +226,14 @@ export class ApplyCodeBlockOperation { return undefined; } - private async handleTextEditor(codeEditor: IActiveCodeEditor, code: string): Promise { + private async handleTextEditor(codeEditor: IActiveCodeEditor, chatSessionId: string | undefined, code: string): Promise { const activeModel = codeEditor.getModel(); if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); return undefined; } - const codeBlock = { code, resource: activeModel.uri, markdownBeforeBlock: undefined }; + const codeBlock = { code, resource: activeModel.uri, chatSessionId, markdownBeforeBlock: undefined }; const codeMapper = this.codeMapperService.providers[0]?.displayName; if (!codeMapper) { @@ -245,7 +247,7 @@ export class ApplyCodeBlockOperation { { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, async progress => { progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) }); - const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token); + const editsIterable = this.getEdits(codeBlock, chatSessionId, cancellationTokenSource.token); return await this.waitForFirstElement(editsIterable); }, () => cancellationTokenSource.cancel() @@ -265,10 +267,11 @@ export class ApplyCodeBlockOperation { }; } - private getEdits(codeBlock: ICodeMapperCodeBlock, token: CancellationToken): AsyncIterable { + private getEdits(codeBlock: ICodeMapperCodeBlock, chatSessionId: string | undefined, token: CancellationToken): AsyncIterable { return new AsyncIterableObject(async executor => { const request: ICodeMapperRequest = { - codeBlocks: [codeBlock] + codeBlocks: [codeBlock], + chatSessionId }; const response: ICodeMapperResponse = { textEdit: (target: URI, edit: TextEdit[]) => { diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts new file mode 100644 index 00000000000..905fe400340 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../chat.js'; +import { CHAT_CATEGORY } from '../chatActions.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize, localize2 } from '../../../../../../nls.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; +import { PromptFilePickers } from './dialogs/askToSelectPrompt/promptFilePickers.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { attachInstructionsFiles, IAttachOptions } from './dialogs/askToSelectPrompt/utils/attachInstructions.js'; + +/** + * Action ID for the `Attach Instruction` action. + */ +const ATTACH_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.attach.instructions'; + +/** + * Options for the {@link AttachInstructionsAction} action. + */ +export interface IAttachInstructionsActionOptions { + + /** + * Target chat widget reference to attach the instruction to. If the reference is + * provided, the command will attach the instruction as attachment of the widget. + * Otherwise, the command will re-use an existing one. + */ + readonly widget?: IChatWidget; + + /** + * Instruction resource `URI` to attach to the chat input, if any. + * If provided the resource will be pre-selected in the prompt picker dialog, + * otherwise the dialog will show the prompts list without any pre-selection. + */ + readonly resource?: URI; + + /** + * Whether to skip the instructions files selection dialog. + * + * Note! if this option is set to `true`, the {@link resource} + * option `must be defined`. + */ + readonly skipSelectionDialog?: boolean; +} + +/** + * Action to attach a prompt to a chat widget input. + */ +class AttachInstructionsAction extends Action2 { + constructor() { + super({ + id: ATTACH_INSTRUCTIONS_ACTION_ID, + title: localize2('attach-instructions.capitalized.ellipses', "Attach Instructions..."), + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + }); + } + + public override async run( + accessor: ServicesAccessor, + options: IAttachInstructionsActionOptions, + ): Promise { + const viewsService = accessor.get(IViewsService); + const promptsService = accessor.get(IPromptsService); + const commandService = accessor.get(ICommandService); + const instaService = accessor.get(IInstantiationService); + + const pickers = instaService.createInstance(PromptFilePickers); + + const { skipSelectionDialog, resource } = options; + + const attachOptions: IAttachOptions = { + widget: options.widget, + viewsService, + commandService, + }; + + if (skipSelectionDialog === true) { + assertDefined( + resource, + 'Resource must be defined when skipping prompt selection dialog.', + ); + + const widget = await attachInstructionsFiles( + [resource], + attachOptions, + ); + + widget.focusInput(); + + return; + } + + // find all prompt files in the user workspace + const promptFiles = await promptsService.listPromptFiles('instructions'); + const placeholder = localize( + 'commands.instructions.select-dialog.placeholder', + 'Select instructions files to attach', + ); + + const instructions = await pickers.selectInstructionsFiles({ promptFiles, resource, placeholder }); + + if (instructions !== undefined) { + const widget = await attachInstructionsFiles( + instructions, + attachOptions, + ); + widget.focusInput(); + } + } +} + +/** + * Runs the `Attach Instructions` action with provided options. We export this + * function instead of {@link ATTACH_INSTRUCTIONS_ACTION_ID} directly to + * encapsulate/enforce the correct options to be passed to the action. + */ +export const runAttachInstructionsAction = async ( + commandService: ICommandService, + options: IAttachInstructionsActionOptions, +): Promise => { + return await commandService.executeCommand( + ATTACH_INSTRUCTIONS_ACTION_ID, + options, + ); +}; + +/** + * Helper to register the `Attach Prompt` action. + */ +export const registerAttachPromptActions = () => { + registerAction2(AttachInstructionsAction); +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts new file mode 100644 index 00000000000..0f5157fc38f --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts @@ -0,0 +1,306 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../chat.js'; +import { CHAT_CATEGORY } from '../chatActions.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { OS } from '../../../../../../base/common/platform.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { ResourceContextKey } from '../../../../../common/contextkeys.js'; +import { KeyCode, KeyMod } from '../../../../../../base/common/keyCodes.js'; +import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; +import { ILocalizedString, localize, localize2 } from '../../../../../../nls.js'; +import { UILabelProvider } from '../../../../../../base/common/keybindingLabels.js'; +import { ICommandAction } from '../../../../../../platform/action/common/action.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; +import { PromptFilePickers } from './dialogs/askToSelectPrompt/promptFilePickers.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { EditorContextKeys } from '../../../../../../editor/common/editorContextKeys.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IRunPromptOptions, runPromptFile } from './dialogs/askToSelectPrompt/utils/runPrompt.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; +import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Condition for the `Run Current Prompt` action. + */ +const EDITOR_ACTIONS_CONDITION = ContextKeyExpr.and( + ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + ResourceContextKey.HasResource, + ResourceContextKey.LangId.isEqualTo(PROMPT_LANGUAGE_ID), +); + +/** + * Keybinding of the action. + */ +const COMMAND_KEY_BINDING = KeyMod.WinCtrl | KeyCode.Slash | KeyMod.Alt; + +/** + * Action ID for the `Run Current Prompt` action. + */ +const RUN_CURRENT_PROMPT_ACTION_ID = 'workbench.action.chat.run.prompt.current'; + +/** + * Action ID for the `Run Prompt...` action. + */ +const RUN_SELECTED_PROMPT_ACTION_ID = 'workbench.action.chat.run.prompt'; + +/** + * Constructor options for the `Run Prompt` base action. + */ +interface IRunPromptBaseActionConstructorOptions { + /** + * ID of the action to be registered. + */ + id: string; + + /** + * Title of the action. + */ + title: ILocalizedString; + + /** + * Icon of the action. + */ + icon: ThemeIcon; + + /** + * Keybinding of the action. + */ + keybinding: number; + + /** + * Alt action of the UI menu item. + */ + alt?: ICommandAction; +} + +/** + * Base class of the `Run Prompt` action. + */ +abstract class RunPromptBaseAction extends Action2 { + constructor( + options: IRunPromptBaseActionConstructorOptions, + ) { + super({ + id: options.id, + title: options.title, + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + icon: options.icon, + keybinding: { + when: ContextKeyExpr.and( + EditorContextKeys.editorTextFocus, + EDITOR_ACTIONS_CONDITION, + ), + weight: KeybindingWeight.WorkbenchContrib, + primary: options.keybinding, + }, + menu: [ + { + id: MenuId.EditorTitleRun, + group: 'navigation', + order: options.alt ? 0 : 1, + alt: options.alt, + when: EDITOR_ACTIONS_CONDITION, + }, + ], + }); + } + + /** + * Executes the run prompt action with provided options. + */ + public async execute( + resource: URI | undefined, + inNewChat: boolean, + accessor: ServicesAccessor, + ): Promise { + const viewsService = accessor.get(IViewsService); + const commandService = accessor.get(ICommandService); + + resource ||= getActivePromptFileUri(accessor); + assertDefined( + resource, + 'Cannot find URI resource for an active text editor.', + ); + + const { widget } = await runPromptFile( + resource, + { + inNewChat, + commandService, + viewsService, + }, + ); + + return widget; + } +} + +const RUN_CURRENT_PROMPT_ACTION_TITLE = localize2( + 'run-prompt.capitalized', + "Run Prompt in Current Chat" +); +const RUN_CURRENT_PROMPT_ACTION_ICON = Codicon.playCircle; + +/** + * The default `Run Current Prompt` action. + */ +class RunCurrentPromptAction extends RunPromptBaseAction { + constructor() { + super({ + id: RUN_CURRENT_PROMPT_ACTION_ID, + title: RUN_CURRENT_PROMPT_ACTION_TITLE, + icon: RUN_CURRENT_PROMPT_ACTION_ICON, + keybinding: COMMAND_KEY_BINDING, + }); + } + + public override async run( + accessor: ServicesAccessor, + resource: URI | undefined, + ): Promise { + return await super.execute( + resource, + false, + accessor, + ); + } +} + +class RunSelectedPromptAction extends Action2 { + constructor() { + super({ + id: RUN_SELECTED_PROMPT_ACTION_ID, + title: localize2('run-prompt.capitalized.ellipses', "Run Prompt..."), + icon: Codicon.bookmark, + f1: true, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + keybinding: { + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + weight: KeybindingWeight.WorkbenchContrib, + primary: COMMAND_KEY_BINDING, + }, + category: CHAT_CATEGORY, + }); + } + + public override async run( + accessor: ServicesAccessor, + ): Promise { + const viewsService = accessor.get(IViewsService); + const promptsService = accessor.get(IPromptsService); + const commandService = accessor.get(ICommandService); + const instaService = accessor.get(IInstantiationService); + + const pickers = instaService.createInstance(PromptFilePickers); + + // find all prompt files in the user workspace + const promptFiles = await promptsService.listPromptFiles('prompt'); + const placeholder = localize( + 'commands.prompt.select-dialog.placeholder', + 'Select the prompt file to run (hold {0}-key to use in new chat)', + UILabelProvider.modifierLabels[OS].ctrlKey + ); + + const result = await pickers.selectPromptFile({ promptFiles, placeholder }); + + if (result === undefined) { + return; + } + + const { promptFile, keyMods } = result; + const runPromptOptions: IRunPromptOptions = { + inNewChat: keyMods.ctrlCmd, + viewsService, + commandService, + }; + const { widget } = await runPromptFile( + promptFile, + runPromptOptions, + ); + widget.focusInput(); + } +} + + +/** + * Gets `URI` of a prompt file open in an active editor instance, if any. + */ +export const getActivePromptFileUri = ( + accessor: ServicesAccessor, +): URI | undefined => { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === PROMPT_LANGUAGE_ID) { + return model.uri; + } + return undefined; +}; + + +/** + * Action ID for the `Run Current Prompt In New Chat` action. + */ +const RUN_CURRENT_PROMPT_IN_NEW_CHAT_ACTION_ID = 'workbench.action.chat.run-in-new-chat.prompt.current'; + +const RUN_IN_NEW_CHAT_ACTION_TITLE = localize2( + 'run-prompt-in-new-chat.capitalized', + "Run Prompt In New Chat", +); + +/** + * Icon for the `Run Current Prompt In New Chat` action. + */ +const RUN_IN_NEW_CHAT_ACTION_ICON = Codicon.play; + +/** + * `Run Current Prompt In New Chat` action. + */ +class RunCurrentPromptInNewChatAction extends RunPromptBaseAction { + constructor() { + super({ + id: RUN_CURRENT_PROMPT_IN_NEW_CHAT_ACTION_ID, + title: RUN_IN_NEW_CHAT_ACTION_TITLE, + icon: RUN_IN_NEW_CHAT_ACTION_ICON, + keybinding: COMMAND_KEY_BINDING | KeyMod.CtrlCmd, + alt: { + id: RUN_CURRENT_PROMPT_ACTION_ID, + title: RUN_CURRENT_PROMPT_ACTION_TITLE, + icon: RUN_CURRENT_PROMPT_ACTION_ICON, + }, + }); + } + + public override async run( + accessor: ServicesAccessor, + resource: URI, + ): Promise { + return await super.execute( + resource, + true, + accessor, + ); + } +} + +/** + * Helper to register all the `Run Current Prompt` actions. + */ +export const registerRunPromptActions = () => { + registerAction2(RunCurrentPromptInNewChatAction); + registerAction2(RunCurrentPromptAction); + registerAction2(RunSelectedPromptAction); +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts new file mode 100644 index 00000000000..52a38b7ed80 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts @@ -0,0 +1,291 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../chat.js'; +import { CHAT_CATEGORY } from '../chatActions.js'; +import { localize2 } from '../../../../../../nls.js'; +import { IEditorPane } from '../../../../../common/editor.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { ILanguageModelToolsService } from '../../../common/languageModelToolsService.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { chatSubcommandLeader, IParsedChatRequest } from '../../../common/chatParserTypes.js'; +import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; + +/** + * Action ID for the `Save Prompt` action. + */ +const SAVE_TO_PROMPT_ACTION_ID = 'workbench.action.chat.save-to-prompt'; + +/** + * Name of the in-chat slash command associated with this action. + */ +export const SAVE_TO_PROMPT_SLASH_COMMAND_NAME = 'save'; + +/** + * Options for the {@link SaveToPromptAction} action. + */ +interface ISaveToPromptActionOptions { + /** + * Chat widget reference to save session of. + */ + chat: IChatWidget; +} + +/** + * Class that defines the `Save Prompt` action. + */ +class SaveToPromptAction extends Action2 { + constructor() { + super({ + id: SAVE_TO_PROMPT_ACTION_ID, + title: localize2( + 'workbench.actions.save-to-prompt.label', + "Save chat session to a prompt file", + ), + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + }); + } + + public async run( + accessor: ServicesAccessor, + options: ISaveToPromptActionOptions, + ): Promise { + const logService = accessor.get(ILogService); + const editorService = accessor.get(IEditorService); + const toolsService = accessor.get(ILanguageModelToolsService); + + const logPrefix = 'save to prompt'; + const { chat } = options; + + const { viewModel } = chat; + assertDefined( + viewModel, + 'No view model found on currently the active chat widget.', + ); + + const { model } = viewModel; + + const turns: ITurn[] = []; + for (const request of model.getRequests()) { + const { message, response: responseModel } = request; + + if (isSaveToPromptSlashCommand(message)) { + continue; + } + + if (responseModel === undefined) { + logService.warn( + `[${logPrefix}]: skipping request '${request.id}' with no response`, + ); + + continue; + } + + const { response } = responseModel; + + const tools = new Set(); + for (const record of response.value) { + if (('toolId' in record === false) || !record.toolId) { + continue; + } + + const tool = toolsService.getTool(record.toolId); + if ((tool === undefined) || (!tool.toolReferenceName)) { + continue; + } + + tools.add(tool.toolReferenceName); + } + + turns.push({ + request: message.text, + response: response.getMarkdown(), + tools, + }); + } + + const promptText = renderPrompt(turns); + + const editor = await editorService.openEditor({ + resource: undefined, + contents: promptText, + languageId: PROMPT_LANGUAGE_ID, + }); + + assertDefined( + editor, + 'Failed to open untitled editor for the prompt.', + ); + + editor.focus(); + + return editor; + } +} + +/** + * Check if provided message belongs to the `save to prompt` slash + * command itself that was run in the chat to invoke this action. + */ +const isSaveToPromptSlashCommand = ( + message: IParsedChatRequest, +): boolean => { + const { parts } = message; + if (parts.length < 1) { + return false; + } + + const firstPart = parts[0]; + if (firstPart.kind !== 'slash') { + return false; + } + + if (firstPart.text !== `${chatSubcommandLeader}${SAVE_TO_PROMPT_SLASH_COMMAND_NAME}`) { + return false; + } + + return true; +}; + +/** + * Render the response part of a `request`/`response` turn pair. + */ +const renderResponse = ( + response: string, +): string => { + // if response starts with a code block, add an extra new line + // before it, to prevent full blockquote from being be broken + const delimiter = (response.startsWith('```')) + ? '\n>' + : ' '; + + // add `>` to the beginning of each line of the response + // so it looks like a blockquote citing Copilot + const quotedResponse = response.replaceAll('\n', '\n> '); + + return `> Copilot:${delimiter}${quotedResponse}`; +}; + +/** + * Render a single `request`/`response` turn of the chat session. + */ +const renderTurn = ( + turn: ITurn, +): string => { + const { request, response } = turn; + + return `\n${request}\n\n${renderResponse(response)}`; +}; + +/** + * Render the entire chat session as a markdown prompt. + */ +const renderPrompt = ( + turns: readonly ITurn[], +): string => { + const content: string[] = []; + const allTools = new Set(); + + // render each turn and collect tool names + // that were used in the each turn + for (const turn of turns) { + content.push(renderTurn(turn)); + + // collect all used tools into a set of strings + for (const tool of turn.tools) { + allTools.add(tool); + } + } + + const result = []; + + // add prompt header + if (allTools.size !== 0) { + result.push(renderHeader(allTools)); + } + + // add chat request/response turns + result.push( + content.join('\n'), + ); + + // add trailing empty line + result.push(''); + + return result.join('\n'); +}; + + +/** + * Render the `tools` metadata inside prompt header. + */ +const renderTools = ( + tools: Set, +): string => { + const toolStrings = [...tools].map((tool) => { + return `'${tool}'`; + }); + + return `tools: [${toolStrings.join(', ')}]`; +}; + +/** + * Render prompt header. + */ +const renderHeader = ( + tools: Set, +): string => { + // skip rendering the header if no tools provided + if (tools.size === 0) { + return ''; + } + + return [ + '---', + renderTools(tools), + '---', + ].join('\n'); +}; + +/** + * Interface for a single `request`/`response` turn + * of a chat session. + */ +interface ITurn { + request: string; + response: string; + tools: Set; +} + +/** + * Runs the `Save To Prompt` action with provided options. We export this + * function instead of {@link SAVE_TO_PROMPT_ACTION_ID} directly to + * encapsulate/enforce the correct options to be passed to the action. + */ +export const runSaveToPromptAction = async ( + options: ISaveToPromptActionOptions, + commandService: ICommandService, +) => { + return await commandService.executeCommand( + SAVE_TO_PROMPT_ACTION_ID, + options, + ); +}; + +/** + * Helper to register all the `Save Prompt` actions. + */ +export const registerSaveToPromptActions = () => { + registerAction2(SaveToPromptAction); +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts new file mode 100644 index 00000000000..e394c1e09c6 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../../../nls.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { OS } from '../../../../../../../../base/common/platform.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { Codicon } from '../../../../../../../../base/common/codicons.js'; +import { WithUriValue } from '../../../../../../../../base/common/types.js'; +import { ThemeIcon } from '../../../../../../../../base/common/themables.js'; +import { IPromptPath } from '../../../../../common/promptSyntax/service/types.js'; +import { dirname, extUri } from '../../../../../../../../base/common/resources.js'; +import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; +import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; +import { UILabelProvider } from '../../../../../../../../base/common/keybindingLabels.js'; +import { IDialogService } from '../../../../../../../../platform/dialogs/common/dialogs.js'; +import { getCleanPromptName } from '../../../../../../../../platform/prompts/common/constants.js'; +import { INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; +import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent } from '../../../../../../../../platform/quickinput/common/quickInput.js'; +import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; +import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID } from '../../../../promptSyntax/contributions/createPromptCommand/createPromptCommand.js'; + +/** + * Options for the {@link askToSelectInstructions} function. + */ +export interface ISelectOptions { + + /** + * The text shows as placeholder in the selection dialog. + */ + readonly placeholder: string; + + /** + * Prompt resource `URI` to attach to the chat input, if any. + * If provided the resource will be pre-selected in the prompt picker dialog, + * otherwise the dialog will show the prompts list without any pre-selection. + */ + readonly resource?: URI; + + /** + * List of prompt files to show in the selection dialog. + */ + readonly promptFiles: readonly IPromptPath[]; +} + +export interface ISelectPromptResult { + /** + * The selected prompt file. + */ + readonly promptFile: URI; + + /** + * The key modifiers that were pressed when the prompt was selected. + */ + readonly keyMods: IKeyMods; +} + +/** + * Button that opems the documentation. + */ +const HELP_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize('help', "help"), + iconClass: ThemeIcon.asClassName(Codicon.question), +}); + +/** + * A quick pick item that starts the 'New Prompt File' command. + */ +const NEW_PROMPT_FILE_OPTION: WithUriValue = Object.freeze({ + type: 'item', + label: `$(plus) ${localize( + 'commands.new-promptfile.select-dialog.label', + 'New prompt file...' + )}`, + value: URI.parse(PROMPT_DOCUMENTATION_URL), + pickable: false, + alwaysShow: true, + buttons: [HELP_BUTTON], +}); + +/** + * A quick pick item that starts the 'New Instructions File' command. + */ +const NEW_INSTRUCTIONS_FILE_OPTION: WithUriValue = Object.freeze({ + type: 'item', + label: `$(plus) ${localize( + 'commands.new-instructionsfile.select-dialog.label', + 'Create new instruction file...', + )}`, + value: URI.parse(INSTRUCTIONS_DOCUMENTATION_URL), + pickable: false, + alwaysShow: true, + buttons: [HELP_BUTTON], +}); + + +/** + * Button that opens a prompt file in the editor. + */ +const EDIT_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize( + 'commands.prompts.use.select-dialog.open-button.tooltip', + "edit ({0}-key + enter)", + UILabelProvider.modifierLabels[OS].ctrlKey + ), + iconClass: ThemeIcon.asClassName(Codicon.edit), +}); + +/** + * Button that deletes a prompt file. + */ +const DELETE_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize('delete', "delete"), + iconClass: ThemeIcon.asClassName(Codicon.trash), +}); + + +export class PromptFilePickers { + constructor( + @ILabelService private readonly _labelService: ILabelService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IOpenerService private readonly _openerService: IOpenerService, + @IFileService private readonly _fileService: IFileService, + @IDialogService private readonly _dialogService: IDialogService, + @ICommandService private readonly _commandService: ICommandService, + ) { + } + /** + * Shows the instructions selection dialog to the user that allows to select a instructions file(s). + * + * If {@link ISelectOptions.resource resource} is provided, the dialog will have + * the resource pre-selected in the prompts list. + */ + public async selectInstructionsFiles(options: ISelectOptions): Promise { + + const fileOptions = this._createPromptPickItems(options); + fileOptions.splice(0, 0, NEW_INSTRUCTIONS_FILE_OPTION); + + const activeItem = options.resource && fileOptions.find(f => extUri.isEqual(f.value, options.resource)); + + const quickPick = this._quickInputService.createQuickPick>(); + quickPick.activeItems = [activeItem ?? NEW_INSTRUCTIONS_FILE_OPTION]; + quickPick.placeholder = options.placeholder; + quickPick.canAcceptInBackground = true; + quickPick.matchOnDescription = true; + quickPick.items = fileOptions; + + return new Promise(resolve => { + const disposables = new DisposableStore(); + + let isResolved = false; + + // then the dialog is hidden or disposed for other reason, + // dispose everything and resolve the main promise + disposables.add({ + dispose() { + quickPick.dispose(); + if (!isResolved) { + resolve(undefined); + isResolved = true; + } + }, + }); + + // handle the prompt `accept` event + disposables.add(quickPick.onDidAccept(async (event) => { + const { selectedItems } = quickPick; + + if (selectedItems[0] === NEW_INSTRUCTIONS_FILE_OPTION) { + await this._commandService.executeCommand(NEW_INSTRUCTIONS_COMMAND_ID); + return; + } + + resolve(selectedItems.map(item => item.value)); + isResolved = true; + + // if user submitted their selection, close the dialog + if (!event.inBackground) { + disposables.dispose(); + } + })); + + // handle the `button click` event on a list item (edit, delete, etc.) + disposables.add(quickPick.onDidTriggerItemButton( + e => this._handleButtonClick(quickPick, e)) + ); + + // when the dialog is hidden, dispose everything + disposables.add(quickPick.onDidHide( + disposables.dispose.bind(disposables), + )); + + // finally, reveal the dialog + quickPick.show(); + }); + } + + /** + * Shows the instructions selection dialog to the user that allows to select a instructions file(s). + * + * If {@link ISelectOptions.resource resource} is provided, the dialog will have + * the resource pre-selected in the prompts list. + */ + public async selectPromptFile(options: ISelectOptions): Promise { + + const fileOptions = this._createPromptPickItems(options); + fileOptions.splice(0, 0, NEW_PROMPT_FILE_OPTION); + + const activeItem = options.resource && fileOptions.find(f => extUri.isEqual(f.value, options.resource)); + + const quickPick = this._quickInputService.createQuickPick>(); + + quickPick.activeItems = [activeItem ?? NEW_PROMPT_FILE_OPTION]; + quickPick.placeholder = options.placeholder; + quickPick.canAcceptInBackground = true; + quickPick.matchOnDescription = true; + quickPick.items = fileOptions; + + + return new Promise(resolve => { + const disposables = new DisposableStore(); + + let isResolved = false; + + // then the dialog is hidden or disposed for other reason, + // dispose everything and resolve the main promise + disposables.add({ + dispose() { + quickPick.dispose(); + if (!isResolved) { + resolve(undefined); + isResolved = true; + } + }, + }); + + // handle the prompt `accept` event + disposables.add(quickPick.onDidAccept(async (event) => { + const { selectedItems } = quickPick; + const { keyMods } = quickPick; + + const selectedItem = selectedItems[0]; + if (selectedItem === NEW_PROMPT_FILE_OPTION) { + await this._commandService.executeCommand(NEW_PROMPT_COMMAND_ID); + return; + } + + if (selectedItem) { + resolve({ promptFile: selectedItem.value, keyMods: { ...keyMods } }); + isResolved = true; + } + + // if user submitted their selection, close the dialog + if (!event.inBackground) { + disposables.dispose(); + } + })); + + // handle the `button click` event on a list item (edit, delete, etc.) + disposables.add(quickPick.onDidTriggerItemButton( + e => this._handleButtonClick(quickPick, e)) + ); + + // when the dialog is hidden, dispose everything + disposables.add(quickPick.onDidHide( + disposables.dispose.bind(disposables), + )); + + // finally, reveal the dialog + quickPick.show(); + }); + } + + private _createPromptPickItems(options: ISelectOptions): WithUriValue[] { + const { promptFiles, resource } = options; + + const fileOptions = promptFiles.map((promptFile) => { + return this._createPromptPickItem(promptFile); + }); + + // if a resource is provided, create an `activeItem` for it to pre-select + // it in the UI, and sort the list so the active item appears at the top + let activeItem: WithUriValue | undefined; + if (resource) { + activeItem = fileOptions.find((file) => { + return extUri.isEqual(file.value, resource); + }); + + // if no item for the `resource` was found, it means that the resource is not + // in the list of prompt files, so add a new item for it; this ensures that + // the currently active prompt file is always available in the selection dialog, + // even if it is not included in the prompts list otherwise(from location setting) + if (!activeItem) { + activeItem = this._createPromptPickItem({ + uri: resource, + // "user" prompts are always registered in the prompts list, hence it + // should be safe to assume that `resource` is not "user" prompt here + storage: 'local', + type: 'instructions', + }); + fileOptions.push(activeItem); + } + + fileOptions.sort((file1, file2) => { + if (extUri.isEqual(file1.value, resource)) { + return -1; + } + + if (extUri.isEqual(file2.value, resource)) { + return 1; + } + + return 0; + }); + } + return fileOptions; + } + + private _createPromptPickItem(promptFile: IPromptPath): WithUriValue { + const { uri, storage } = promptFile; + const fileWithoutExtension = getCleanPromptName(uri); + + // if a "user" prompt, don't show its filesystem path in + // the user interface, but do that for all the "local" ones + const description = (storage === 'user') + ? localize('user-data-dir.capitalized', 'User data folder') + : this._labelService.getUriLabel(dirname(uri), { relative: true }); + + const tooltip = (storage === 'user') + ? description + : uri.fsPath; + + return { + id: uri.toString(), + type: 'item', + label: fileWithoutExtension, + description, + tooltip, + value: uri, + buttons: [EDIT_BUTTON, DELETE_BUTTON], + }; + } + + private async _handleButtonClick(quickPick: IQuickPick>, context: IQuickPickItemButtonEvent>) { + const { item, button } = context; + const { value } = item; + + // `edit` button was pressed, open the prompt file in editor + if (button === EDIT_BUTTON) { + return await this._openerService.open(value); + } + + // `delete` button was pressed, delete the prompt file + if (button === DELETE_BUTTON) { + // sanity check to confirm our expectations + assert( + (quickPick.activeItems.length < 2), + `Expected maximum one active item, got '${quickPick.activeItems.length}'.`, + ); + + const activeItem: WithUriValue | undefined = quickPick.activeItems[0]; + + // sanity checks - prompt file exists and is not a folder + const info = await this._fileService.stat(value); + assert( + info.isDirectory === false, + `'${value.fsPath}' points to a folder.`, + ); + + // don't close the main prompt selection dialog by the confirmation dialog + const previousIgnoreFocusOut = quickPick.ignoreFocusOut; + quickPick.ignoreFocusOut = true; + + const filename = getCleanPromptName(value); + const { confirmed } = await this._dialogService.confirm({ + message: localize( + 'commands.prompts.use.select-dialog.delete-prompt.confirm.message', + "Are you sure you want to delete '{0}'?", + filename, + ), + }); + + // restore the previous value of the `ignoreFocusOut` property + quickPick.ignoreFocusOut = previousIgnoreFocusOut; + + // if prompt deletion was not confirmed, nothing to do + if (!confirmed) { + return; + } + + // prompt deletion was confirmed so delete the prompt file + await this._fileService.del(value); + + // remove the deleted prompt from the selection dialog list + let removedIndex = -1; + quickPick.items = quickPick.items.filter((option, index) => { + if (option === item) { + removedIndex = index; + + return false; + } + + return true; + }); + + // if the deleted item was active item, find a new item to set as active + if (activeItem && (activeItem === item)) { + assert( + removedIndex >= 0, + 'Removed item index must be a valid index.', + ); + + // we set the previous item as new active, or the next item + // if removed prompt item was in the beginning of the list + const newActiveItemIndex = Math.max(removedIndex - 1, 0); + const newActiveItem: WithUriValue | undefined = quickPick.items[newActiveItemIndex]; + + quickPick.activeItems = newActiveItem ? [newActiveItem] : []; + } + + return; + } + + if (button === HELP_BUTTON) { + // open the documentation + await this._openerService.open(item.value); + return; + } + + throw new Error(`Unknown button '${JSON.stringify(button)}'.`); + } + +} diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts new file mode 100644 index 00000000000..14bd701edee --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget, showChatView } from '../../../../../chat.js'; +import { URI } from '../../../../../../../../../base/common/uri.js'; +import { ACTION_ID_NEW_CHAT } from '../../../../chatClearActions.js'; +import { assertDefined } from '../../../../../../../../../base/common/types.js'; +import { IAttachInstructionsActionOptions } from '../../../chatAttachInstructionsAction.js'; +import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; +import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; + +/** + * Options for the {@link attachInstructionsFiles} function. + */ +export interface IAttachOptions { + /** + * Chat widget instance to attach the prompt to. + */ + readonly widget?: IChatWidget; + /** + * Whether to create a new chat session and + * attach the prompt to it. + */ + readonly inNewChat?: boolean; + + readonly viewsService: IViewsService; + readonly commandService: ICommandService; +} + +/** + * Attaches provided instructions to a chat input. + */ +export const attachInstructionsFiles = async ( + files: URI[], + options: IAttachOptions, +): Promise => { + const widget = await getChatWidgetObject(options); + + for (const file of files) { + widget.attachmentModel.promptInstructions.add(file); + } + + return widget; +}; + +/** + * Gets a chat widget based on the provided {@link IAttachInstructionsActionOptions.widget widget} + * reference and the `inNewChat` flag. + * + * @throws if failed to reveal a chat widget. + */ +export const getChatWidgetObject = async ( + options: IAttachOptions, +): Promise => { + const { widget, inNewChat } = options; + + // if a new chat sessions needs to be created, or there is no + // chat widget reference provided, show a chat view, otherwise + // re-use the existing chat widget + if ((inNewChat === true) || (widget === undefined)) { + return await showChat(options, inNewChat); + } + + return widget; +}; + +/** + * Reveals an existing one or creates a new one based on + * the provided `createNew` flag. + */ +const showChat = async ( + options: IAttachOptions, + createNew: boolean = false, +): Promise => { + const { commandService, viewsService } = options; + + if (createNew === true) { + await commandService.executeCommand(ACTION_ID_NEW_CHAT); + } + + const widget = await showChatView(viewsService); + + assertDefined( + widget, + 'Chat widget must be defined.', + ); + + return widget; +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts new file mode 100644 index 00000000000..4f56b99be1b --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../../../../chat.js'; +import { URI } from '../../../../../../../../../base/common/uri.js'; + +/** + * Options for the {@link detachPrompt} function. + */ +export interface IDetachPromptOptions { + /** + * Chat widget instance to attach the prompt to. + */ + readonly widget: IChatWidget; +} + +/** + * Detaches provided prompts to a chat input. + */ +export const detachPrompt = async ( + file: URI, + options: IDetachPromptOptions, +): Promise => { + const { widget } = options; + + widget + .attachmentModel + .promptInstructions + .remove(file); + + return widget; +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts new file mode 100644 index 00000000000..f3a49ad2f3d --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../../../../chat.js'; +import { getChatWidgetObject } from './attachInstructions.js'; +import { URI } from '../../../../../../../../../base/common/uri.js'; +import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; +import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; +import { getPromptCommandName } from '../../../../../../common/promptSyntax/service/promptsService.js'; + +/** + * Options for the {@link runPromptFile} function. + */ +export interface IRunPromptOptions { + /** + * Chat widget instance to attach the prompt to. + */ + readonly widget?: IChatWidget; + /** + * Whether to create a new chat session and + * attach the instructions file to it. + */ + readonly inNewChat?: boolean; + + readonly viewsService: IViewsService; + readonly commandService: ICommandService; +} + +/** + * Return value of the {@link runPromptFile} function. + */ +interface IRunPromptResult { + readonly widget: IChatWidget; +} + +/** + * Runs the prompt file. + */ +export const runPromptFile = async ( + file: URI, + options: IRunPromptOptions, +): Promise => { + + const widget = await getChatWidgetObject(options); + + widget.setInput(`/${getPromptCommandName(file.path)}`); + // submit the prompt immediately + await widget.acceptInput(); + + + return { widget }; +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts new file mode 100644 index 00000000000..629a85d1ee8 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerRunPromptActions } from './chatRunPromptAction.js'; +import { registerSaveToPromptActions } from './chatSaveToPromptAction.js'; +import { registerAttachPromptActions } from './chatAttachInstructionsAction.js'; +export { runAttachInstructionsAction } from './chatAttachInstructionsAction.js'; + +/** + * Helper to register all actions related to reusable prompt files. + */ +export const registerPromptActions = () => { + registerRunPromptActions(); + registerAttachPromptActions(); + registerSaveToPromptActions(); +}; diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 73ab2960823..e620ed6bc7a 100644 --- a/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -10,6 +10,7 @@ import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/ho import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { basename, dirname } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; @@ -52,6 +53,10 @@ export class ImplicitContextAttachmentWidget extends Disposable { dom.clearNode(this.domNode); this.renderDisposables.clear(); + const attachmentTypeName = (this.attachment.isPromptFile === false) + ? localize('file.lowercase', "file") + : localize('prompt.lowercase', "prompt"); + this.domNode.classList.toggle('disabled', !this.attachment.enabled); const label = this.resourceLabels.create(this.domNode, { supportIcons: true }); const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value!.uri; @@ -60,25 +65,33 @@ export class ImplicitContextAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); + const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached {0}, {1}, line {2} to line {3}", attachmentTypeName, friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached {0}, {1}", attachmentTypeName, friendlyName); const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const currentFile = localize('openEditor', "Current file context"); + const currentFile = localize('openEditor', "Current {0} context", attachmentTypeName); const inactive = localize('enableHint', "disabled"); const currentFileHint = currentFile + (this.attachment.enabled ? '' : ` (${inactive})`); const title = `${currentFileHint}\n${uriLabel}`; + + const icon = this.attachment.isPromptFile + ? ThemeIcon.fromId(Codicon.bookmark.id) + : undefined; + label.setFile(file, { fileKind: FileKind.FILE, hidePath: true, range, - title + title, + icon, }); this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, 'Current file')); + + const hintLabel = localize('hint.label.current', "Current {0}", attachmentTypeName); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, hintLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); - const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current file context") : localize('enable', "Enable current file context"); + const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName); const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg })); toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed; this.renderDisposables.add(toggleButton.onDidClick((e) => { diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts similarity index 72% rename from code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts rename to code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts index d05558a0489..8c13e9f61ac 100644 --- a/code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts @@ -6,34 +6,37 @@ import { URI } from '../../../../../../base/common/uri.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; -import { PromptAttachmentWidget } from './promptAttachmentWidget.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { InstructionsAttachmentWidget } from './promptInstructionsWidget.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ChatPromptAttachmentsCollection } from '../../chatAttachmentModel/chatPromptAttachmentsCollection.js'; /** * Widget for a collection of prompt instructions attachments. - * See {@linkcode PromptAttachmentWidget}. + * See {@link InstructionsAttachmentWidget}. */ -export class PromptAttachmentsCollectionWidget extends Disposable { +export class PromptInstructionsAttachmentsCollectionWidget extends Disposable { /** * List of child instruction attachment widgets. */ - private children: PromptAttachmentWidget[] = []; + private children: InstructionsAttachmentWidget[] = []; /** * Event that fires when number of attachments change * - * See {@linkcode onAttachmentsCountChange}. + * See {@link onAttachmentsChange}. */ - private _onAttachmentsCountChange = this._register(new Emitter()); + private _onAttachmentsChange = this._register(new Emitter()); /** - * Subscribe to the `onAttachmentsCountChange` event. + * Subscribe to the `onAttachmentsChange` event. * @param callback Function to invoke when number of attachments change. */ - public onAttachmentsCountChange(callback: () => unknown): this { - this._register(this._onAttachmentsCountChange.event(callback)); + public onAttachmentsChange(callback: () => unknown): this { + this._register(this._onAttachmentsChange.event(callback)); return this; } @@ -59,6 +62,14 @@ export class PromptAttachmentsCollectionWidget extends Disposable { return this.model.chatAttachments; } + /** + * Get a promise that resolves when parsing/resolving processes + * are fully completed, including all possible nested child references. + */ + public allSettled() { + return this.model.allSettled(); + } + /** * Check if child widget list is empty (no attachments present). */ @@ -66,10 +77,23 @@ export class PromptAttachmentsCollectionWidget extends Disposable { return this.children.length === 0; } + /** + * Check if any of the attachments is a prompt file. + */ + public get hasInstructions(): boolean { + return this.references.some((uri) => { + const model = this.modelService.getModel(uri); + const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri); + return languageId === INSTRUCTIONS_LANGUAGE_ID; + }); + } + constructor( private readonly model: ChatPromptAttachmentsCollection, private readonly resourceLabels: ResourceLabels, @IInstantiationService private readonly initService: IInstantiationService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, @ILogService private readonly logService: ILogService, ) { super(); @@ -77,9 +101,9 @@ export class PromptAttachmentsCollectionWidget extends Disposable { this.render = this.render.bind(this); // when a new attachment model is added, create a new child widget for it - this.model.onAdd((attachment) => { + this._register(this.model.onAdd((attachment) => { const widget = this.initService.createInstance( - PromptAttachmentWidget, + InstructionsAttachmentWidget, attachment, this.resourceLabels, ); @@ -90,22 +114,22 @@ export class PromptAttachmentsCollectionWidget extends Disposable { // register the new child widget this.children.push(widget); - // if parent node is present - append the wiget to it, otherwise wait + // if parent node is present - append the widget to it, otherwise wait // until the `render` method will be called if (this.parentNode) { this.parentNode.appendChild(widget.domNode); } // fire the event to notify about the change in the number of attachments - this._onAttachmentsCountChange.fire(); - }); + this._onAttachmentsChange.fire(); + })); } /** * Handle child widget disposal. * @param widget The child widget that was disposed. */ - public handleAttachmentDispose(widget: PromptAttachmentWidget): this { + public handleAttachmentDispose(widget: InstructionsAttachmentWidget): this { // common prefix for all log messages const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`; @@ -148,7 +172,7 @@ export class PromptAttachmentsCollectionWidget extends Disposable { this.parentNode?.removeChild(widget.domNode); // fire the event to notify about the change in the number of attachments - this._onAttachmentsCountChange.fire(); + this._onAttachmentsChange.fire(); return this; } diff --git a/code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts b/code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts similarity index 92% rename from code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts rename to code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts index ac4fcefcd20..e03aeb95a21 100644 --- a/code/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts @@ -31,7 +31,7 @@ import { getFlatContextMenuActions } from '../../../../../../platform/actions/br /** * Widget for a single prompt instructions attachment. */ -export class PromptAttachmentWidget extends Disposable { +export class InstructionsAttachmentWidget extends Disposable { /** * The root DOM node of the widget. */ @@ -106,12 +106,18 @@ export class PromptAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = localize('chat.promptAttachment', "Prompt attachment, {0}", friendlyName); + const isPrompt = this.languageService.guessLanguageIdByFilepathOrFirstLine(file) === 'prompt'; + const ariaLabel = isPrompt + ? localize('chat.promptAttachment', "Prompt file, {0}", friendlyName) + : localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName); + const typeLabel = isPrompt + ? localize('prompt', "Prompt") + : localize('instructions', "Instructions"); + const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const promptLabel = localize('prompt', "Prompt"); - let title = `${promptLabel} ${uriLabel}`; + let title = `${typeLabel} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -144,7 +150,7 @@ export class PromptAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, promptLabel)); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, typeLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index db1a22b9789..3ccbe624aa7 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -9,19 +9,17 @@ import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlCo import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; +import { assertDefined } from '../../../../base/common/types.js'; import { registerEditorFeature } from '../../../../editor/common/editorFeatures.js'; import * as nls from '../../../../nls.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationNode, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import product from '../../../../platform/product/common/product.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; import { PromptsConfig } from '../../../../platform/prompts/common/config.js'; -import { DEFAULT_SOURCE_FOLDER as PROMPT_FILES_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; @@ -34,7 +32,6 @@ import { allDiscoverySources, discoverySourceLabel, mcpConfigurationSection, mcp import { ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { CodeMapperService, ICodeMapperService } from '../common/chatCodeMapperService.js'; import '../common/chatColors.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingService } from '../common/chatEditingService.js'; import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; import { chatVariableLeader } from '../common/chatParserTypes.js'; @@ -49,10 +46,8 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; -import '../common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.js'; -import '../common/promptSyntax/languageFeatures/promptLinkProvider.js'; -import '../common/promptSyntax/languageFeatures/promptPathAutocompletion.js'; +import { INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; +import { registerPromptFileContributions } from '../common/promptSyntax/contributions/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; import { IPromptsService } from '../common/promptSyntax/service/types.js'; import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js'; @@ -74,6 +69,7 @@ import { registerQuickChatActions } from './actions/chatQuickInputActions.js'; import { registerChatTitleActions } from './actions/chatTitleActions.js'; import { registerChatToolActions } from './actions/chatToolActions.js'; import { ChatTransferContribution } from './actions/chatTransfer.js'; +import { SAVE_TO_PROMPT_SLASH_COMMAND_NAME, runSaveToPromptAction } from './actions/promptActions/chatSaveToPromptAction.js'; import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './chatAccessibilityService.js'; import './chatAttachmentModel.js'; @@ -85,6 +81,7 @@ import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorCon import { ChatEditingEditorOverlay } from './chatEditing/chatEditingEditorOverlay.js'; import { ChatEditingService } from './chatEditing/chatEditingServiceImpl.js'; import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditing/notebook/chatEditingNotebookFileSystemProvider.js'; +import { SimpleBrowserOverlay } from './chatEditing/simpleBrowserEditorOverlay.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; @@ -103,8 +100,8 @@ import './contrib/chatInputEditorContrib.js'; import './contrib/chatInputEditorHover.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { LanguageModelToolsService } from './languageModelToolsService.js'; +import './promptSyntax/contributions/attachInstructionsCommand.js'; import './promptSyntax/contributions/createPromptCommand/createPromptCommand.js'; -import './promptSyntax/contributions/usePromptCommand.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; // Register configuration @@ -161,7 +158,6 @@ configurationRegistry.registerConfiguration({ }, default: { 'panel': 'always', - 'editing-session': 'first' } }, 'chat.editing.autoAcceptDelay': { @@ -199,12 +195,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.renderRelatedFiles', "Controls whether related files should be rendered in the chat input."), default: false }, - 'chat.setupFromDialog': { // TODO@bpasero remove this eventually - type: 'boolean', - description: nls.localize('chat.setupFromChat', "Controls whether Copilot setup starts from a dialog or from the welcome view."), - default: product.quality !== 'stable', - tags: ['experimental', 'onExp'] - }, 'chat.focusWindowOnConfirmation': { type: 'boolean', description: nls.localize('chat.focusWindowOnConfirmation', "Controls whether the Copilot window should be focused when a confirmation is needed."), @@ -212,7 +202,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.tools.autoApprove': { default: false, - description: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved ('YOLO mode')."), + description: nls.localize('chat.tools.autoApprove', "Controls whether tool use should be automatically approved."), type: 'boolean', tags: ['experimental'], policy: { @@ -222,6 +212,24 @@ configurationRegistry.registerConfiguration({ defaultValue: false } }, + 'chat.sendElementsToChat.enabled': { + default: true, + description: nls.localize('chat.sendElementsToChat.enabled', "Controls whether elements can be sent to chat from the Simple Browser."), + type: 'boolean', + tags: ['experimental'] + }, + 'chat.sendElementsToChat.attachCSS': { + default: true, + markdownDescription: nls.localize('chat.sendElementsToChat.attachCSS', "Controls whether CSS of the selected element will be added to the chat. {0} must be enabled.", '`#chat.sendElementsToChat.enabled#`'), + type: 'boolean', + tags: ['experimental'] + }, + 'chat.sendElementsToChat.attachImages': { + default: true, + markdownDescription: nls.localize('chat.sendElementsToChat.attachImages', "Controls whether a screenshot of the selected element will be added to the chat. {0} must be enabled.", '`#chat.sendElementsToChat.enabled#`'), + type: 'boolean', + tags: ['experimental'] + }, [mcpEnabledSection]: { type: 'boolean', description: nls.localize('chat.mcp.enabled', "Enables integration with Model Context Protocol servers to provide additional tools and functionality."), @@ -243,12 +251,6 @@ configurationRegistry.registerConfiguration({ description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"), $ref: mcpSchemaId }, - [ChatConfiguration.UnifiedChatView]: { - type: 'boolean', - description: nls.localize('chat.unifiedChatView', "Enables the unified view with Ask, Edit, and Agent modes in one view."), - default: true, - tags: ['preview'], - }, [ChatConfiguration.UseFileStorage]: { type: 'boolean', description: nls.localize('chat.useFileStorage', "Enables storing chat sessions on disk instead of in the storage service. Enabling this does a one-time per-workspace migration of existing sessions to the new format."), @@ -263,16 +265,28 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.ExtensionToolsEnabled]: { type: 'boolean', - description: nls.localize('chat.extensionToolsEnabled', "Enable using tools contributed by third-party extensions in Copilot Chat agent mode."), + description: nls.localize('chat.extensionToolsEnabled', "Enable using tools contributed by third-party extensions."), default: true, policy: { name: 'ChatAgentExtensionTools', minimumVersion: '1.99', - description: nls.localize('chat.extensionToolsPolicy', "Enable using tools contributed by third-party extensions in Copilot Chat agent mode."), + description: nls.localize('chat.extensionToolsPolicy', "Enable using tools contributed by third-party extensions."), previewFeature: true, defaultValue: false } }, + [ChatConfiguration.AgentEnabled]: { + type: 'boolean', + description: nls.localize('chat.agent.enabled.description', "Enable agent mode for {0}. When this is enabled, agent mode can be activated via the dropdown in the view.", 'Copilot Chat'), + default: true, + tags: ['onExp'], + policy: { + name: 'ChatAgentMode', + minimumVersion: '1.99', + previewFeature: false, + defaultValue: false + } + }, [mcpDiscoverySection]: { oneOf: [ { type: 'boolean' }, @@ -296,9 +310,10 @@ configurationRegistry.registerConfiguration({ ), markdownDescription: nls.localize( 'chat.reusablePrompts.config.enabled.description', - "Enable reusable prompt files (`*{0}`) in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", + "Enable reusable prompt (`*{0}`) and instruction files in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", PROMPT_FILE_EXTENSION, - DOCUMENTATION_URL, + INSTRUCTION_FILE_EXTENSION, + PROMPT_DOCUMENTATION_URL, ), default: true, restricted: true, @@ -307,12 +322,40 @@ configurationRegistry.registerConfiguration({ policy: { name: 'ChatPromptFiles', minimumVersion: '1.99', - description: nls.localize('chat.promptFiles.policy', "Enables reusable prompt files in Chat, Edits, and Inline Chat sessions."), + description: nls.localize('chat.promptFiles.policy', "Enables reusable prompt and instruction files in Chat, Edits, and Inline Chat sessions."), previewFeature: true, defaultValue: false } }, - [PromptsConfig.LOCATIONS_KEY]: { + [PromptsConfig.INSTRUCTIONS_LOCATION_KEY]: { + type: 'object', + title: nls.localize( + 'chat.instructions.config.locations.title', + "Instructions File Locations", + ), + markdownDescription: nls.localize( + 'chat.instructions.config.locations.description', + "Specify location(s) of instructions files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", + INSTRUCTION_FILE_EXTENSION, + INSTRUCTIONS_DOCUMENTATION_URL, + ), + default: { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + }, + additionalProperties: { type: 'boolean' }, + restricted: true, + tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], + examples: [ + { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + }, + { + [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + '/Users/vscode/repos/instructions': true, + }, + ], + }, + [PromptsConfig.PROMPT_LOCATIONS_KEY]: { type: 'object', title: nls.localize( 'chat.reusablePrompts.config.locations.title', @@ -320,12 +363,12 @@ configurationRegistry.registerConfiguration({ ), markdownDescription: nls.localize( 'chat.reusablePrompts.config.locations.description', - "Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", + "Specify location(s) of reusable prompt files (`*{0}`) that can be run in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", PROMPT_FILE_EXTENSION, - DOCUMENTATION_URL, + PROMPT_DOCUMENTATION_URL, ), default: { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, }, additionalProperties: { type: 'boolean' }, unevaluatedProperties: { type: 'boolean' }, @@ -333,10 +376,10 @@ configurationRegistry.registerConfiguration({ tags: ['experimental', 'prompts', 'reusable prompts', 'prompt snippets', 'instructions'], examples: [ { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, }, { - [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, + [PROMPT_DEFAULT_SOURCE_FOLDER]: true, '/Users/vscode/repos/prompts': true, }, ], @@ -397,68 +440,14 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr static readonly ID = 'workbench.contrib.chatAgentSetting'; - private registeredNode: IConfigurationNode | undefined; - constructor( @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, - @IProductService private readonly productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService, @IChatEntitlementService private readonly entitlementService: IChatEntitlementService, ) { super(); - - if (this.productService.quality !== 'stable') { - this.registerEnablementSetting(); - } - - const expDisabledKey = ChatContextKeys.Editing.agentModeDisallowed.bindTo(contextKeyService); - experimentService.getTreatment('chatAgentEnabled').then(enabled => { - if (enabled || typeof enabled !== 'boolean') { - // If enabled, or experiments not available, fall back to registering the setting - this.registerEnablementSetting(); - expDisabledKey.set(false); - } else { - // If disabled, deregister the setting - this.deregisterSetting(); - expDisabledKey.set(true); - } - }); - this.registerMaxRequestsSetting(); } - private registerEnablementSetting() { - if (this.registeredNode) { - return; - } - - this.registeredNode = configurationRegistry.registerConfiguration({ - id: 'chatAgent', - title: nls.localize('interactiveSessionConfigurationTitle', "Chat"), - type: 'object', - properties: { - [ChatConfiguration.AgentEnabled]: { - type: 'boolean', - description: nls.localize('chat.agent.enabled.description', "Enable agent mode for {0}. When this is enabled, a dropdown appears in the view to toggle agent mode.", 'Copilot Chat'), - default: this.productService.quality !== 'stable', - tags: ['onExp'], - policy: { - name: 'ChatAgentMode', - minimumVersion: '1.99', - previewFeature: false, - defaultValue: false - } - }, - } - }); - } - - private deregisterSetting() { - if (this.registeredNode) { - configurationRegistry.deregisterConfigurations([this.registeredNode]); - this.registeredNode = undefined; - } - } private registerMaxRequestsSetting(): void { let lastNode: IConfigurationNode | undefined; @@ -475,7 +464,7 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr properties: { 'chat.agent.maxRequests': { type: 'number', - markdownDescription: nls.localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use per-turn in agent mode. When the limit is reached, Copilot will ask the user to confirm that it should keep working. \n\n> **Note**: For users on the Copilot Free plan, note that each agent mode request currently uses one chat request."), + markdownDescription: nls.localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use per-turn in agent mode. When the limit is reached, Copilot will ask the user to confirm that it should keep working."), default: defaultValue, }, } @@ -504,7 +493,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @IChatSlashCommandService slashCommandService: IChatSlashCommandService, @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, - @IChatVariablesService chatVariablesService: IChatVariablesService, + @IChatWidgetService chatWidgetService: IChatWidgetService, @IInstantiationService instantiationService: IInstantiationService, ) { super(); @@ -517,6 +506,22 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { }, async () => { commandService.executeCommand(ACTION_ID_NEW_CHAT); })); + this._store.add(slashCommandService.registerSlashCommand({ + command: SAVE_TO_PROMPT_SLASH_COMMAND_NAME, + detail: nls.localize('save-chat-to-prompt-file', "Save chat to a prompt file"), + sortText: `z3_${SAVE_TO_PROMPT_SLASH_COMMAND_NAME}`, + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Panel] + }, async () => { + const { lastFocusedWidget } = chatWidgetService; + assertDefined( + lastFocusedWidget, + 'No currently active chat widget found.', + ); + + runSaveToPromptAction({ chat: lastFocusedWidget }, commandService); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', detail: '', @@ -606,9 +611,10 @@ registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingSta registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatStatusBarEntry.ID, ChatStatusBarEntry, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); @@ -631,6 +637,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 +658,7 @@ 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); + +registerPromptFileContributions(); diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.ts b/code/src/vs/workbench/contrib/chat/browser/chat.ts index 45a3078e6b7..9db1a4c3aee 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.ts @@ -11,8 +11,7 @@ import { Selection } from '../../../../editor/common/core/selection.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js'; import { IChatResponseModel } from '../common/chatModel.js'; @@ -49,18 +48,6 @@ export async function showChatView(viewsService: IViewsService): Promise(ChatViewId))?.widget; } -export async function showEditsView(viewsService: IViewsService): Promise { - return (await viewsService.openView(EditsViewId))?.widget; -} - -export function preferCopilotEditsView(viewsService: IViewsService): boolean { - if (viewsService.getFocusedView()?.id === ChatViewId || !!viewsService.getActiveViewWithId(ChatViewId)) { - return false; - } - - return !!viewsService.getActiveViewWithId(EditsViewId); -} - export function showCopilotView(viewsService: IViewsService, layoutService: IWorkbenchLayoutService): Promise { // Ensure main window is in front @@ -68,35 +55,7 @@ export function showCopilotView(viewsService: IViewsService, layoutService: IWor layoutService.mainContainer.focus(); } - // Bring up the correct view - if (preferCopilotEditsView(viewsService)) { - return showEditsView(viewsService); - } else { - return showChatView(viewsService); - } -} - -export function ensureSideBarChatViewSize(viewDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService, viewsService: IViewsService): void { - const viewId = preferCopilotEditsView(viewsService) ? EditsViewId : ChatViewId; - - const location = viewDescriptorService.getViewLocationById(viewId); - if (location === ViewContainerLocation.Panel) { - return; // panel is typically very wide - } - - const viewPart = location === ViewContainerLocation.Sidebar ? Parts.SIDEBAR_PART : Parts.AUXILIARYBAR_PART; - const partSize = layoutService.getSize(viewPart); - - let adjustedChatWidth: number | undefined; - if (partSize.width < 400 && layoutService.mainContainerDimension.width > 1200) { - adjustedChatWidth = 400; // up to 400px if window bounds permit - } else if (partSize.width < 300) { - adjustedChatWidth = 300; // at minimum 300px - } - - if (typeof adjustedChatWidth === 'number') { - layoutService.setSize(viewPart, { width: adjustedChatWidth, height: partSize.height }); - } + return showChatView(viewsService); } export const IQuickChatService = createDecorator('quickChatService'); @@ -142,6 +101,7 @@ export interface IChatCodeBlockInfo { readonly uriPromise: Promise; codemapperUri: URI | undefined; readonly isStreaming: boolean; + readonly chatSessionId: string; focus(): void; } @@ -169,7 +129,6 @@ export interface IChatWidgetViewOptions { renderFollowups?: boolean; renderStyle?: 'compact' | 'minimal'; supportsFileReferences?: boolean; - supportsAdditionalParticipants?: boolean; filter?: (item: ChatTreeItem) => boolean; rendererOptions?: IChatListItemRendererOptions; menus?: { @@ -191,6 +150,7 @@ export interface IChatWidgetViewOptions { enableImplicitContext?: boolean; enableWorkingSet?: 'explicit' | 'implicit'; supportsChangingModes?: boolean; + dndContainer?: HTMLElement; } export interface IChatViewViewContext { @@ -209,6 +169,7 @@ export interface IChatAcceptInputOptions { } export interface IChatWidget { + readonly domNode: HTMLElement; readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; readonly onDidHide: Event; @@ -226,8 +187,7 @@ export interface IChatWidget { readonly input: ChatInputPart; readonly attachmentModel: ChatAttachmentModel; - // TODO I don't like this - readonly isUnifiedPanelWidget: boolean; + readonly supportsChangingModes: boolean; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; @@ -250,6 +210,11 @@ export interface IChatWidget { getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; clear(): void; + /** + * Wait for this widget to have a VM with a fully initialized model and editing session. + * Sort of a hack. See https://github.com/microsoft/vscode/issues/247484 + */ + waitForReady(): Promise; getViewState(): IChatViewState; togglePaused(): void; } @@ -267,5 +232,3 @@ export interface IChatCodeBlockContextProviderService { } export const ChatViewId = `workbench.panel.chat.view.${CHAT_PROVIDER_ID}`; - -export const EditsViewId = 'workbench.panel.chat.view.edits'; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 957cba60d4e..8c1bd1164f6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -11,11 +11,29 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ChatTreeItem } from './chat.js'; import { isRequestVM, isResponseVM, IChatResponseViewModel } from '../common/chatViewModel.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { AcceptToolConfirmationActionId } from './actions/chatToolActions.js'; +import { CancelChatActionId } from './actions/chatExecuteActions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; + +export const getToolConfirmationAlert = (accessor: ServicesAccessor, title: string) => { + const keybindingService = accessor.get(IKeybindingService); + const contextKeyService = accessor.get(IContextKeyService); + + const acceptKb = keybindingService.lookupKeybinding(AcceptToolConfirmationActionId, contextKeyService)?.getAriaLabel(); + const cancelKb = keybindingService.lookupKeybinding(CancelChatActionId, contextKeyService)?.getAriaLabel(); + + return acceptKb && cancelKb + ? localize('toolInvocationsHintKb', "Action required to confirm tool action: {0}. Press {1} to accept or {2} to cancel.", title, acceptKb, cancelKb) + : localize('toolInvocationsHint', "Action required to confirm tool action: {0}", title); +}; export class ChatAccessibilityProvider implements IListAccessibilityProvider { constructor( - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { } getWidgetRole(): AriaRole { @@ -46,12 +64,19 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider v.kind === 'toolInvocation').filter(v => !v.isComplete); + const toolInvocation = element.response.value.filter(v => v.kind === 'toolInvocation'); let toolInvocationHint = ''; if (toolInvocation.length) { - const titles = toolInvocation.map(v => v.confirmationMessages?.title).filter(v => !!v); - if (titles.length) { - toolInvocationHint = localize('toolInvocationsHint', "Action required: {0} ", titles.join(', ')); + const waitingForConfirmation = toolInvocation.filter(v => !v.isComplete); + if (waitingForConfirmation.length) { + const titles = toolInvocation.map(v => v.confirmationMessages?.title).filter(v => !!v); + if (titles.length) { + toolInvocationHint = this._instantiationService.invokeFunction(getToolConfirmationAlert, titles.join(', ')); + } + } else { // all completed + for (const invocation of toolInvocation) { + toolInvocationHint += localize('toolCompletedHint', "Tool {0} completed.", invocation.confirmationMessages?.title); + } } } const tableCount = marked.lexer(element.response.toString()).filter(token => token.type === 'table')?.length ?? 0; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index a61ab242d16..11d44a0edb1 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -12,70 +12,88 @@ import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ChatPromptAttachmentsCollection } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { resizeImage } from './imageUtils.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { localize } from '../../../../nls.js'; +import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { resolveImageEditorAttachContext } from './chatAttachmentResolve.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { equals } from '../../../../base/common/objects.js'; + +export interface IChatAttachmentChangeEvent { + readonly deleted: readonly string[]; + readonly added: readonly IChatRequestVariableEntry[]; + readonly updated: readonly IChatRequestVariableEntry[]; +} export class ChatAttachmentModel extends Disposable { - /** - * Collection on prompt instruction attachments. - */ - public readonly promptInstructions: ChatPromptAttachmentsCollection; + + readonly promptInstructions: ChatPromptAttachmentsCollection; + private readonly _attachments = new Map(); + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; constructor( - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService instaService: IInstantiationService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, + @ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService, ) { super(); - this.promptInstructions = this._register( - this.initService.createInstance(ChatPromptAttachmentsCollection), - ).onUpdate(() => { - this._onDidChangeContext.fire(); - }); + this.promptInstructions = this._register(instaService.createInstance(ChatPromptAttachmentsCollection)); } - private _attachments = new Map(); get attachments(): ReadonlyArray { return Array.from(this._attachments.values()); } - protected _onDidChangeContext = this._register(new Emitter()); - readonly onDidChangeContext = this._onDidChangeContext.event; + get size(): number { return this._attachments.size; } get fileAttachments(): URI[] { - return this.attachments.reduce((acc, file) => { - if (file.isFile && URI.isUri(file.value)) { - acc.push(file.value); - } - return acc; - }, []); + return this.attachments.filter(file => file.kind === 'file' && URI.isUri(file.value)) + .map(file => file.value as URI); } getAttachmentIDs() { return new Set(this._attachments.keys()); } - clear(): void { + clear(clearStickyAttachments: boolean = false): void { + const deleted = Array.from(this._attachments.keys()); this._attachments.clear(); - this._onDidChangeContext.fire(); + + if (clearStickyAttachments) { + this.promptInstructions.clear(); + } + + this._onDidChange.fire({ deleted, added: [], updated: [] }); } delete(...variableEntryIds: string[]) { + const deleted: string[] = []; + for (const variableEntryId of variableEntryIds) { - this._attachments.delete(variableEntryId); + if (this._attachments.delete(variableEntryId)) { + deleted.push(variableEntryId); + } + } + + if (deleted.length > 0) { + this._onDidChange.fire({ deleted, added: [], updated: [] }); } - this._onDidChangeContext.fire(); } async addFile(uri: URI, range?: IRange) { if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(uri.path)) { - this.addContext(await this.asImageVariableEntry(uri)); + const context = await this.asImageVariableEntry(uri); + if (context) { + this.addContext(context); + } return; } @@ -84,59 +102,90 @@ export class ChatAttachmentModel extends Disposable { addFolder(uri: URI) { this.addContext({ + kind: 'directory', value: uri, id: uri.toString(), name: basename(uri), - isFile: false, - isDirectory: true, }); } asVariableEntry(uri: URI, range?: IRange): IChatRequestVariableEntry { return { + kind: 'file', value: range ? { uri, range } : uri, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri), - isFile: true, }; } - async asImageVariableEntry(uri: URI): Promise { - const fileName = basename(uri); - const readFile = await this.fileService.readFile(uri); - if (readFile.size > 30 * 1024 * 1024) { // 30 MB - this.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'); + // Gets an image variable for a given URI, which may be a file or a web URL + async asImageVariableEntry(uri: URI): Promise { + if (uri.scheme === Schemas.file && await this.fileService.canHandleResource(uri)) { + return await resolveImageEditorAttachContext(this.fileService, this.dialogService, uri); + } else if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) { + const extractedImages = await this.webContentExtractorService.readImage(uri, CancellationToken.None); + if (extractedImages) { + return await resolveImageEditorAttachContext(this.fileService, this.dialogService, uri, extractedImages); + } } - const resizedImage = await resizeImage(readFile.value.buffer); - return { - id: uri.toString(), - name: fileName, - fullName: uri.path, - value: resizedImage, - isImage: true, - isFile: false, - references: [{ reference: uri, kind: 'reference' }] - }; + + return undefined; } addContext(...attachments: IChatRequestVariableEntry[]) { - let hasAdded = false; + const added: IChatRequestVariableEntry[] = []; for (const attachment of attachments) { if (!this._attachments.has(attachment.id)) { this._attachments.set(attachment.id, attachment); - hasAdded = true; + added.push(attachment); } } - if (hasAdded) { - this._onDidChangeContext.fire(); + if (added.length > 0) { + this._onDidChange.fire({ deleted: [], added, updated: [] }); } } clearAndSetContext(...attachments: IChatRequestVariableEntry[]) { - this.clear(); - this.addContext(...attachments); + const deleted = Array.from(this._attachments.keys()); + this._attachments.clear(); + + const added: IChatRequestVariableEntry[] = []; + for (const attachment of attachments) { + this._attachments.set(attachment.id, attachment); + added.push(attachment); + } + + if (deleted.length > 0 || added.length > 0) { + this._onDidChange.fire({ deleted, added, updated: [] }); + } + } + + updateContent(toDelete: Iterable, upsert: Iterable) { + const deleted: string[] = []; + const added: IChatRequestVariableEntry[] = []; + const updated: IChatRequestVariableEntry[] = []; + + for (const id of toDelete) { + if (this._attachments.delete(id)) { + deleted.push(id); + } + } + + for (const item of upsert) { + const oldItem = this._attachments.get(item.id); + if (!oldItem) { + this._attachments.set(item.id, item); + added.push(item); + } else if (!equals(oldItem, item)) { + this._attachments.set(item.id, item); + updated.push(item); + } + } + + if (deleted.length > 0 || added.length > 0 || updated.length > 0) { + this._onDidChange.fire({ deleted, added, updated }); + } } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts index bb41b884ec6..3227f8e8984 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts @@ -6,9 +6,16 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { FilePromptParser } from '../../common/promptSyntax/parsers/filePromptParser.js'; +import { PromptParser } from '../../common/promptSyntax/parsers/promptParser.js'; +import { BasePromptParser } from '../../common/promptSyntax/parsers/basePromptParser.js'; +import { IPromptContentsProvider } from '../../common/promptSyntax/contentProviders/types.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +/** + * Type for a generic prompt parser object. + */ +type TPromptParser = BasePromptParser; + /** * Model for a single chat prompt instructions attachment. */ @@ -17,11 +24,12 @@ export class ChatPromptAttachmentModel extends Disposable { * Private reference of the underlying prompt instructions * reference instance. */ - private readonly _reference: FilePromptParser; + private readonly _reference: TPromptParser; + /** * Get the prompt instructions reference instance. */ - public get reference(): FilePromptParser { + public get reference(): TPromptParser { return this._reference; } @@ -31,7 +39,7 @@ export class ChatPromptAttachmentModel extends Disposable { */ public get references(): readonly URI[] { const { reference } = this; - const { errorCondition } = this.reference; + const { errorCondition } = reference; // return no references if the attachment is disabled // or if this object itself has an error @@ -47,11 +55,24 @@ export class ChatPromptAttachmentModel extends Disposable { ]; } + /** + * Get list of all tools associated with the prompt. + * + * Note! This property returns pont-in-time state of the tools metadata + * and does not take into account if the prompt or its nested child + * references are still being resolved. Please use the {@link settled} + * or {@link allSettled} properties if you need to retrieve the final + * list of the tools available. + */ + public get toolsMetadata(): readonly string[] | null { + return this.reference.allToolsMetadata; + } + /** * Promise that resolves when the prompt is fully parsed, * including all its possible nested child references. */ - public get allSettled(): Promise { + public get allSettled(): Promise { return this.reference.allSettled(); } @@ -67,7 +88,7 @@ export class ChatPromptAttachmentModel extends Disposable { * Event that fires when the error condition of the prompt * reference changes. * - * See {@linkcode onUpdate}. + * See {@link onUpdate}. */ protected _onUpdate = this._register(new Emitter()); /** @@ -83,7 +104,7 @@ export class ChatPromptAttachmentModel extends Disposable { /** * Event that fires when the object is disposed. * - * See {@linkcode onDispose}. + * See {@link onDispose}. */ protected _onDispose = this._register(new Emitter()); /** @@ -97,14 +118,25 @@ export class ChatPromptAttachmentModel extends Disposable { } constructor( - uri: URI, + public readonly uri: URI, @IInstantiationService private readonly initService: IInstantiationService, ) { super(); - this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); - this._reference = this._register(this.initService.createInstance(FilePromptParser, uri, [])) - .onUpdate(this._onUpdate.fire); + this._reference = this._register( + this.initService.createInstance( + PromptParser, + this.uri, + // in this case we know that the attached file must have been a + // prompt file, hence we pass the `allowNonPromptFiles` option + // to the provider to allow for non-prompt files to be attached + { allowNonPromptFiles: true }, + ) + ); + + this._reference.onUpdate( + this._onUpdate.fire.bind(this._onUpdate), + ); } /** diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts index 64445d15277..471d468650e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts @@ -5,13 +5,43 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { IChatRequestVariableEntry } from '../../common/chatModel.js'; +import { basename } from '../../../../../base/common/resources.js'; import { ChatPromptAttachmentModel } from './chatPromptAttachmentModel.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IChatRequestVariableEntry, IPromptVariableEntry, isChatRequestFileEntry } from '../../common/chatModel.js'; + +/** + * Prefix for all prompt instruction variable IDs. + */ +const PROMPT_VARIABLE_ID_PREFIX = 'vscode.prompt.instructions'; + +/** + * Prompt IDs start with a well-defined prefix that is used by + * the copilot extension to identify prompt references. + * + * @param uri The URI of the prompt file. + * @param isRoot Whether the prompt file is the root file, or a + * child reference that is nested inside the root file. + */ +export const createPromptVariableId = ( + uri: URI, + isRoot: boolean, +): string => { + // the default prefix that is used for all prompt files + let prefix = PROMPT_VARIABLE_ID_PREFIX; + // if the reference is the root object, add the `.root` suffix + if (isRoot) { + prefix += '.root'; + } + + // final `id` for all `prompt files` starts with the well-defined + // part that the copilot extension(or other chatbot) can rely on + return `${prefix}__${uri}`; +}; /** * Utility to convert a {@link reference} to a chat variable entry. @@ -28,41 +58,81 @@ import { IConfigurationService } from '../../../../../platform/configuration/com export const toChatVariable = ( reference: Pick, isRoot: boolean, -): IChatRequestVariableEntry => { - const { uri, isPromptFile: isPromptFile } = reference; +): IPromptVariableEntry => { + const { uri, isPromptFile } = reference; // default `id` is the stringified `URI` let id = `${uri}`; - // for prompt files, we add a prefix to the `id` + // prompts have special `id`s that are used by the copilot extension if (isPromptFile) { - // the default prefix that is used for all prompt files - let prefix = 'vscode.prompt.instructions'; - // if the reference is the root object, add the `.root` suffix - if (isRoot) { - prefix += '.root'; - } - - // final `id` for all `prompt files` starts with the well-defined - // part that the copilot extension(or other chatbot) can rely on - id = `${prefix}__${id}`; + id = createPromptVariableId(uri, isRoot); } + const name = (isPromptFile) + ? `prompt:${basename(uri)}` + : `file:${basename(uri)}`; + + const modelDescription = (isPromptFile) + ? 'Prompt instructions file' + : 'File attachment'; + return { id, - name: uri.fsPath, + name, value: uri, - isSelection: false, - enabled: true, - isFile: true, + kind: 'file', + modelDescription, + isRoot, }; }; +/** + * Checks of a provided chat variable is a `prompt file` variable. + */ +export function isPromptFileChatVariable( + variable: IChatRequestVariableEntry, +): variable is IPromptVariableEntry { + return isChatRequestFileEntry(variable) + && variable.id.startsWith(PROMPT_VARIABLE_ID_PREFIX); +} + /** * Model for a collection of prompt instruction attachments. * See {@linkcode ChatPromptAttachmentModel} for individual attachment. */ export class ChatPromptAttachmentsCollection extends Disposable { + /** + * Event that fires then this model is updated. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + */ + public onUpdate = this._onUpdate.event; + + /** + * Event that fires when a new prompt instruction attachment is added. + * See {@linkcode onAdd}. + */ + protected _onAdd = this._register(new Emitter()); + /** + * The `onAdd` event fires when a new prompt instruction attachment is added. + */ + public onAdd = this._onAdd.event; + + /** + * Event that fires when a new prompt instruction attachment is removed. + * See {@linkcode onRemove}. + */ + protected _onRemove = this._register(new Emitter()); + /** + * The `onRemove` event fires when a new prompt instruction attachment is removed. + */ + public onRemove = this._onRemove.event; + /** * List of all prompt instruction attachments. */ @@ -83,6 +153,26 @@ export class ChatPromptAttachmentsCollection extends Disposable { return result; } + /** + * Get list of tools associated with all attached prompt files. + */ + public get toolsMetadata(): readonly string[] | null { + const result = []; + + for (const child of this.attachments.values()) { + const { toolsMetadata } = child; + + if (toolsMetadata === null) { + continue; + } + + result.push(...toolsMetadata); + } + + // return unique list of all tools + return [...new Set(result)]; + } + /** * Get the list of all prompt instruction attachment variables, including all * nested child references of each attachment explicitly attached by user. @@ -95,7 +185,7 @@ export class ChatPromptAttachmentsCollection extends Disposable { const { reference } = attachment; // the usual URIs list of prompt instructions is `bottom-up`, therefore - // we do the same herfe - first add all child references of the model + // we do the same here - first add all child references of the model result.push( ...reference.allValidReferences.map((link) => { return toChatVariable(link, false); @@ -104,7 +194,13 @@ export class ChatPromptAttachmentsCollection extends Disposable { // then add the root reference of the model itself result.push( - toChatVariable(reference, true), + toChatVariable({ + uri: reference.uri, + // the attached file must have been a prompt file therefore + // we force that assumption here; this makes sure that prompts + // in untitled documents can be also attached to the chat input + isPromptFile: true, + }, true), ); } @@ -115,7 +211,7 @@ export class ChatPromptAttachmentsCollection extends Disposable { * Promise that resolves when parsing of all attached prompt instruction * files completes, including parsing of all its possible child references. */ - public async allSettled(): Promise { + public async allSettled(): Promise { const attachments = [...this.attachments.values()]; await Promise.allSettled( @@ -123,36 +219,6 @@ export class ChatPromptAttachmentsCollection extends Disposable { return attachment.allSettled; }), ); - } - - /** - * Event that fires then this model is updated. - * - * See {@linkcode onUpdate}. - */ - protected _onUpdate = this._register(new Emitter()); - /** - * Subscribe to the `onUpdate` event. - * @param callback Function to invoke on update. - */ - public onUpdate(callback: () => unknown): this { - this._register(this._onUpdate.event(callback)); - - return this; - } - - /** - * Event that fires when a new prompt instruction attachment is added. - * See {@linkcode onAdd}. - */ - protected _onAdd = this._register(new Emitter()); - /** - * The `onAdd` event fires when a new prompt instruction attachment is added. - * - * @param callback Function to invoke on add. - */ - public onAdd(callback: (attachment: ChatPromptAttachmentModel) => unknown): this { - this._register(this._onAdd.event(callback)); return this; } @@ -170,28 +236,34 @@ export class ChatPromptAttachmentsCollection extends Disposable { * Add a prompt instruction attachment instance with the provided `URI`. * @param uri URI of the prompt instruction attachment to add. */ - public add(uri: URI): this { - // if already exists, nothing to do - if (this.attachments.has(uri.path)) { - return this; - } + public add(uris: URI | readonly URI[]) { + const uriList = Array.isArray(uris) ? uris : [uris]; - const instruction = this.initService.createInstance(ChatPromptAttachmentModel, uri) - .onUpdate(this._onUpdate.fire) - .onDispose(() => { - // note! we have to use `deleteAndLeak` here, because the `*AndDispose` - // alternative results in an infinite loop of calling this callback - this.attachments.deleteAndLeak(uri.path); - this._onUpdate.fire(); - }); - - this.attachments.set(uri.path, instruction); - instruction.resolve(); - - this._onAdd.fire(instruction); - this._onUpdate.fire(); + // if no URIs provided, nothing to do + if (uriList.length === 0) { + return; + } - return this; + for (const uri of uriList) { + // if already exists, nothing to do + if (this.attachments.has(uri.path)) { + continue; + } + + const instruction = this.initService.createInstance(ChatPromptAttachmentModel, uri) + .onUpdate(this._onUpdate.fire) + .onDispose(() => { + // note! we have to use `deleteAndLeak` here, because the `*AndDispose` + // alternative results in an infinite loop of calling this callback + this.attachments.deleteAndLeak(uri.path); + this._onUpdate.fire(); + this._onRemove.fire(instruction); + }).resolve(); + + this.attachments.set(uri.path, instruction); + this._onAdd.fire(instruction); + this._onUpdate.fire(); + } } /** @@ -215,4 +287,16 @@ export class ChatPromptAttachmentsCollection extends Disposable { public get featureEnabled(): boolean { return PromptsConfig.enabled(this.configService); } + + /** + * Clear all prompt instruction attachments. + */ + public clear(): this { + for (const attachment of this.attachments.values()) { + this.remove(attachment.uri); + } + + this._onUpdate.fire(); + return this; + } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts new file mode 100644 index 00000000000..16b2d8d00cf --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { 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, NotebookCellOutputTransferData } 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 { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../notebook/browser/contrib/chat/notebookChatUtils.js'; +import { getOutputViewModelFromId } from '../../notebook/browser/controller/cellOutputActions.js'; +import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, OmittedState } 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(fileService, dialogService, editor.resource); + 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 omittedState = OmittedState.NotOmitted; + + if (!isDirectory) { + try { + const createdModel = await textModelService.createModelReference(resource); + createdModel.dispose(); + } catch { + omittedState = OmittedState.Full; + } + + if (/\.(svg)$/i.test(resource.path)) { + omittedState = OmittedState.Full; + } + } + + return { + kind: isDirectory ? 'directory' : 'file', + value: resource, + id: resource.toString(), + name: basename(resource), + omittedState + }; +} + +// --- IMAGES --- + +export type ImageTransferData = { + data: Uint8Array; + name: string; + icon?: ThemeIcon; + resource?: URI; + id?: string; + mimeType?: string; + omittedState?: OmittedState; +}; +const SUPPORTED_IMAGE_EXTENSIONS_REGEX = /\.(png|jpg|jpeg|gif|webp)$/i; + +export async function resolveImageEditorAttachContext(fileService: IFileService, dialogService: IDialogService, resource: URI, data?: VSBuffer): Promise { + if (!resource) { + return undefined; + } + + const match = SUPPORTED_IMAGE_EXTENSIONS_REGEX.exec(resource.path); + if (!match) { + return undefined; + } + + const mimeType = getMimeTypeFromPath(match); + const fileName = basename(resource); + + let dataBuffer: VSBuffer | undefined; + if (data) { + dataBuffer = data; + } else { + + let stat; + try { + stat = await fileService.stat(resource); + } catch { + return undefined; + } + + const readFile = await fileService.readFile(resource); + + if (stat.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'); + } + + dataBuffer = readFile.value; + } + + const isPartiallyOmitted = /\.gif$/i.test(resource.path); + const imageFileContext = await resolveImageAttachContext([{ + id: resource.toString(), + name: fileName, + data: dataBuffer.buffer, + icon: Codicon.fileMedia, + resource: resource, + mimeType: mimeType, + omittedState: isPartiallyOmitted ? OmittedState.Partial : OmittedState.NotOmitted + }]); + + 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, image.mimeType), + icon: image.icon, + kind: 'image', + isFile: false, + isDirectory: false, + omittedState: image.omittedState || OmittedState.NotOmitted, + references: image.resource ? [{ reference: image.resource, kind: 'reference' }] : [] + }))); +} + +const MIME_TYPES: Record = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', +}; + +function getMimeTypeFromPath(match: RegExpExecArray): string | undefined { + const ext = match[1].toLowerCase(); + return MIME_TYPES[ext]; +} + +export function getAttachableImageExtension(mimeType: string): string | undefined { + return Object.entries(MIME_TYPES).find(([_, value]) => value === mimeType)?.[0]; +} + +// --- 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, + icon: SymbolKinds.toIcon(symbol.kind), + fullName: 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; +} + +// --- NOTEBOOKS --- + +export function resolveNotebookOutputAttachContext(data: NotebookCellOutputTransferData, editorService: IEditorService): IChatRequestVariableEntry[] { + const notebookEditor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + if (!notebookEditor) { + return []; + } + + const outputViewModel = getOutputViewModelFromId(data.outputId, notebookEditor); + if (!outputViewModel) { + return []; + } + + const mimeType = outputViewModel.pickedMimeType?.mimeType; + if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) { + + const entry = createNotebookOutputVariableEntry(outputViewModel, mimeType, notebookEditor); + if (!entry) { + return []; + } + + return [entry]; + } + + return []; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index 1873df4b042..6739ee84b3a 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { $, addDisposableListener } from '../../../../base/browser/dom.js'; +import * as event from '../../../../base/common/event.js'; +import { $ } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -25,7 +25,7 @@ import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js'; import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js'; import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, OmittedState } from '../common/chatModel.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; @@ -33,13 +33,17 @@ import { basename, dirname } from '../../../../base/common/path.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { INotebookService } from '../../notebook/common/notebookService.js'; +import { CellUri } from '../../notebook/common/notebookCommon.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; abstract class AbstractChatAttachmentWidget extends Disposable { public readonly element: HTMLElement; public readonly label: IResourceLabel; - private readonly _onDidDelete: Emitter = this._register(new Emitter()); - get onDidDelete(): Event { + private readonly _onDidDelete: event.Emitter = this._register(new event.Emitter()); + get onDidDelete(): event.Event { return this._onDidDelete.event; } @@ -61,10 +65,17 @@ abstract class AbstractChatAttachmentWidget extends Disposable { } protected modelSupportsVision() { - return this.currentLanguageModel?.metadata.capabilities?.vision ?? false; + return modelSupportsVision(this.currentLanguageModel); } protected attachClearButton() { + + if (this.attachment.range) { + // no clear button for attachments with ranges because range means + // referenced from prompt + return; + } + const clearButton = new Button(this.element, { supportIcons: true, hoverDelegate: this.hoverDelegate, @@ -72,9 +83,14 @@ abstract class AbstractChatAttachmentWidget extends Disposable { }); clearButton.icon = Codicon.close; this._register(clearButton); - this._register(Event.once(clearButton.onDidClick)((e) => { + this._register(event.Event.once(clearButton.onDidClick)((e) => { this._onDidDelete.fire(e); })); + this._register(dom.addStandardDisposableListener(this.element, dom.EventType.KEY_DOWN, e => { + if (e.keyCode === KeyCode.Backspace || e.keyCode === KeyCode.Delete) { + this._onDidDelete.fire(e.browserEvent); + } + })); if (this.shouldFocusClearButton) { clearButton.focus(); } @@ -84,7 +100,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable { this.element.style.cursor = 'pointer'; this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, true); - if (this.attachment.isDirectory) { + if (this.attachment.kind === 'directory') { this.openResource(resource, true); } else { this.openResource(resource, false, range); @@ -95,7 +111,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { dom.EventHelper.stop(e, true); - if (this.attachment.isDirectory) { + if (this.attachment.kind === 'directory') { this.openResource(resource, true); } else { this.openResource(resource, false, range); @@ -123,6 +139,10 @@ abstract class AbstractChatAttachmentWidget extends Disposable { } } +function modelSupportsVision(currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined) { + return currentLanguageModel?.metadata.capabilities?.vision ?? false; +} + export class FileAttachmentWidget extends AbstractChatAttachmentWidget { constructor( @@ -146,14 +166,14 @@ export class FileAttachmentWidget extends AbstractChatAttachmentWidget { const fileBasename = basename(resource.path); const fileDirname = dirname(resource.path); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); - this.element.ariaLabel = ariaLabel; + let ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); - if (attachment.isOmitted) { + if (attachment.omittedState === OmittedState.Full) { + ariaLabel = localize('chat.omittedFileAttachment', "Omitted this file: {0}", attachment.name); this.renderOmittedWarning(friendlyName, ariaLabel, hoverDelegate); } else { const fileOptions: IFileLabelOptions = { hidePath: true }; - this.label.setFile(resource, attachment.isFile ? { + this.label.setFile(resource, attachment.kind === 'file' ? { ...fileOptions, fileKind: FileKind.FILE, range, @@ -164,6 +184,8 @@ export class FileAttachmentWidget extends AbstractChatAttachmentWidget { }); } + this.element.ariaLabel = ariaLabel; + this.instantiationService.invokeFunction(accessor => { this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource)); }); @@ -184,7 +206,6 @@ export class FileAttachmentWidget extends AbstractChatAttachmentWidget { hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this {1} type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name : this.currentLanguageModel, 'file'); this._register(this.hoverService.setupManagedHover(hoverDelegate, this.element, hoverElement, { trapFocus: true })); - } } @@ -206,28 +227,22 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { ) { super(attachment, shouldFocusClearButton, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); - const ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - this.element.ariaLabel = ariaLabel; - this.element.style.position = 'relative'; - - if (attachment.references) { - this.element.style.cursor = 'pointer'; - const clickHandler = () => { - if (attachment.references && URI.isUri(attachment.references[0].reference)) { - this.openResource(attachment.references[0].reference, false, undefined); - } - }; - this._register(addDisposableListener(this.element, 'click', clickHandler)); + let ariaLabel: string; + if (attachment.omittedState === OmittedState.Full) { + ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name); + } else if (attachment.omittedState === OmittedState.Partial) { + ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name); + } else { + ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); } - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(this.modelSupportsVision() ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); - const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.name); - this.element.appendChild(pillIcon); - this.element.appendChild(textLabel); - - const hoverElement = dom.$('div.chat-attached-context-hover'); - hoverElement.setAttribute('aria-label', ariaLabel); - + const ref = attachment.references?.[0]?.reference; + resource = ref && URI.isUri(ref) ? ref : undefined; + const clickHandler = () => { + if (resource) { + this.openResource(resource, false, undefined); + } + }; type AttachImageEvent = { currentModel: string; supportsVision: boolean; @@ -247,15 +262,8 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { supportsVision: supportsVision }); - if (!supportsVision && this.currentLanguageModel) { - this.element.classList.add('warning'); - hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this {1} type.", currentLanguageModelName, 'image'); - this._register(this.hoverService.setupManagedHover(hoverDelegate, this.element, hoverElement, { trapFocus: true })); - } else { - const buffer = attachment.value as Uint8Array; - this.createImageElements(buffer, this.element, hoverElement); - this._register(this.hoverService.setupManagedHover(hoverDelegate, this.element, hoverElement, { trapFocus: false })); - } + const fullName = resource?.toString() || attachment.fullName || attachment.name; + this._register(createImageElements(resource, attachment.name, fullName, this.element, attachment.value as Uint8Array, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState)); if (resource) { this.addResourceOpenHandlers(resource, undefined); @@ -263,37 +271,79 @@ export class ImageAttachmentWidget extends AbstractChatAttachmentWidget { this.attachClearButton(); } +} + +function createImageElements(resource: URI | undefined, name: string, fullName: string, + element: HTMLElement, + buffer: ArrayBuffer | Uint8Array, + hoverService: IHoverService, ariaLabel: string, + currentLanguageModelName: string, + clickHandler: () => void, + currentLanguageModel?: ILanguageModelChatMetadataAndIdentifier, + omittedState?: OmittedState): IDisposable { + + const disposable = new DisposableStore(); + if (omittedState === OmittedState.Partial) { + element.classList.add('partial-warning'); + } + + element.ariaLabel = ariaLabel; + element.style.position = 'relative'; + + if (resource) { + element.style.cursor = 'pointer'; + disposable.add(dom.addDisposableListener(element, 'click', clickHandler)); + } + const supportsVision = modelSupportsVision(currentLanguageModel); + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); + const textLabel = dom.$('span.chat-attached-context-custom-text', {}, name); + element.appendChild(pillIcon); + element.appendChild(textLabel); + + const hoverElement = dom.$('div.chat-attached-context-hover'); + hoverElement.setAttribute('aria-label', ariaLabel); + + if (!supportsVision && currentLanguageModel) { + element.classList.add('warning'); + hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this {1} type.", currentLanguageModelName, 'image'); + disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } })); + } else { + disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } })); + - private createImageElements(buffer: ArrayBuffer | Uint8Array, widget: HTMLElement, hoverElement: HTMLElement) { const blob = new Blob([buffer], { type: 'image/png' }); const url = URL.createObjectURL(blob); const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' }); const pill = dom.$('div.chat-attached-context-pill', {}, pillImg); - const existingPill = widget.querySelector('.chat-attached-context-pill'); + const existingPill = element.querySelector('.chat-attached-context-pill'); if (existingPill) { existingPill.replaceWith(pill); } const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' }); + const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage); + hoverElement.appendChild(imageContainer); - // Update hover image - hoverElement.appendChild(hoverImage); - - hoverImage.onload = () => { - URL.revokeObjectURL(url); - }; + if (resource) { + const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame will be sent.") : fullName); + const separator = dom.$('div.chat-attached-context-url-separator'); + disposable.add(dom.addDisposableListener(urlContainer, 'click', () => clickHandler())); + hoverElement.append(separator, urlContainer); + } + hoverImage.onload = () => { URL.revokeObjectURL(url); }; hoverImage.onerror = () => { // reset to original icon on error or invalid image const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon); - const existingPill = widget.querySelector('.chat-attached-context-pill'); + const existingPill = element.querySelector('.chat-attached-context-pill'); if (existingPill) { existingPill.replaceWith(pill); } }; } + return disposable; } export class PasteAttachmentWidget extends AbstractChatAttachmentWidget { @@ -342,7 +392,7 @@ export class PasteAttachmentWidget extends AbstractChatAttachmentWidget { const copiedFromResource = attachment.copiedFrom?.uri; if (copiedFromResource) { - this._register(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, copiedFromResource))); + this._register(this.instantiationService.invokeFunction(hookUpResourceAttachmentDragAndContextMenu, this.element, copiedFromResource)); this.addResourceOpenHandlers(copiedFromResource, range); } @@ -368,7 +418,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { super(attachment, shouldFocusClearButton, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); const attachmentLabel = attachment.fullName ?? attachment.name; - const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel; this.label.setLabel(withIcon, undefined); this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); @@ -386,7 +436,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { if (attachment.kind === 'symbol') { const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); - this._register(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext))); + this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext)); } if (resource) { @@ -396,3 +446,139 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { this.attachClearButton(); } } + +export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachmentWidget { + constructor( + resource: URI, + attachment: INotebookOutputVariableEntry, + currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined, + shouldFocusClearButton: boolean, + container: HTMLElement, + contextResourceLabels: ResourceLabels, + hoverDelegate: IHoverDelegate, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService, + @IHoverService private readonly hoverService: IHoverService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @INotebookService private readonly notebookService: INotebookService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(attachment, shouldFocusClearButton, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); + + switch (attachment.mimeType) { + case 'application/vnd.code.notebook.error': { + this.renderErrorOutput(resource, attachment); + break; + } + case 'image/png': + case 'image/jpeg': + case 'image/svg': { + this.renderImageOutput(resource, attachment); + break; + } + default: { + this.renderGenericOutput(resource, attachment); + } + } + + this.instantiationService.invokeFunction(accessor => { + this._register(hookUpResourceAttachmentDragAndContextMenu(accessor, this.element, resource)); + }); + this.addResourceOpenHandlers(resource, undefined); + this.attachClearButton(); + } + getAriaLabel(attachment: INotebookOutputVariableEntry): string { + return localize('chat.NotebookImageAttachment', "Attached Notebook output, {0}", attachment.name); + } + private renderErrorOutput(resource: URI, attachment: INotebookOutputVariableEntry) { + const attachmentLabel = attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel; + const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array(); + let title: string | undefined = undefined; + try { + const error = JSON.parse(new TextDecoder().decode(buffer)) as Error; + if (error.name && error.message) { + title = `${error.name}: ${error.message}`; + } + } catch { + // + } + this.label.setLabel(withIcon, undefined, { title }); + this.element.ariaLabel = this.getAriaLabel(attachment); + } + private renderGenericOutput(resource: URI, attachment: INotebookOutputVariableEntry) { + this.element.ariaLabel = this.getAriaLabel(attachment); + this.label.setFile(resource, { hidePath: true, icon: ThemeIcon.fromId('output') }); + } + private renderImageOutput(resource: URI, attachment: INotebookOutputVariableEntry) { + let ariaLabel: string; + if (attachment.omittedState === OmittedState.Full) { + ariaLabel = localize('chat.omittedNotebookImageAttachment', "Omitted this Notebook ouput: {0}", attachment.name); + } else if (attachment.omittedState === OmittedState.Partial) { + ariaLabel = localize('chat.partiallyOmittedNotebookImageAttachment', "Partially omitted this Notebook output: {0}", attachment.name); + } else { + ariaLabel = this.getAriaLabel(attachment); + } + + const clickHandler = () => this.openResource(resource, false, undefined); + const currentLanguageModelName = this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel.identifier)?.name ?? this.currentLanguageModel.identifier : 'unknown'; + const buffer = this.getOutputItem(resource, attachment)?.data.buffer ?? new Uint8Array(); + this._register(createImageElements(resource, attachment.name, attachment.name, this.element, buffer, this.hoverService, ariaLabel, currentLanguageModelName, clickHandler, this.currentLanguageModel, attachment.omittedState)); + } + + private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) { + const parsedInfo = CellUri.parseCellOutputUri(resource); + if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') { + return undefined; + } + const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook); + if (!notebook) { + return undefined; + } + const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle); + if (!cell) { + return undefined; + } + const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined; + return output?.outputs.find(o => o.mime === attachment.mimeType); + } + +} + +export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget { + constructor( + attachment: IElementVariableEntry, + currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined, + shouldFocusClearButton: boolean, + container: HTMLElement, + contextResourceLabels: ResourceLabels, + hoverDelegate: IHoverDelegate, + @ICommandService commandService: ICommandService, + @IOpenerService openerService: IOpenerService, + @IEditorService editorService: IEditorService, + ) { + super(attachment, shouldFocusClearButton, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService); + + const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name); + this.element.ariaLabel = ariaLabel; + + this.element.style.position = 'relative'; + this.element.style.cursor = 'pointer'; + const attachmentLabel = attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel; + this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) }); + + this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => { + const content = attachment.value?.toString() || ''; + await editorService.openEditor({ + resource: undefined, + contents: content, + options: { + pinned: true + } + }); + })); + + this.attachClearButton(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index aaf5e335bcd..9bbf2168612 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -11,6 +11,7 @@ import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { basename, dirname } from '../../../../../base/common/path.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; @@ -38,8 +39,11 @@ import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/co import { fillEditorsDragData } from '../../../../browser/dnd.js'; import { ResourceLabels } from '../../../../browser/labels.js'; import { ResourceContextKey } from '../../../../common/contextkeys.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { revealInSideBarCommand } from '../../../files/browser/fileActions.contribution.js'; -import { IChatRequestVariableEntry, isImageVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js'; +import { CellUri } from '../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { IChatRequestVariableEntry, INotebookOutputVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, OmittedState } from '../../common/chatModel.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { convertUint8ArrayToString } from '../imageUtils.js'; @@ -63,6 +67,8 @@ export class ChatAttachmentsContentPart extends Disposable { @ICommandService private readonly commandService: ICommandService, @IThemeService private readonly themeService: IThemeService, @ILabelService private readonly labelService: ILabelService, + @INotebookService private readonly notebookService: INotebookService, + @IEditorService private readonly editorService: IEditorService, ) { super(); @@ -92,34 +98,23 @@ export class ChatAttachmentsContentPart extends Disposable { let ariaLabel: string | undefined; - if (resource && (attachment.isFile || attachment.isDirectory)) { - const fileBasename = basename(resource.path); - const fileDirname = dirname(resource.path); - const friendlyName = `${fileBasename} ${fileDirname}`; - - if (isAttachmentOmitted) { - ariaLabel = range ? localize('chat.omittedFileAttachmentWithRange', "Omitted: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.omittedFileAttachment', "Omitted: {0}.", friendlyName); - } else if (isAttachmentPartialOrOmitted) { - ariaLabel = range ? localize('chat.partialFileAttachmentWithRange', "Partially attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.partialFileAttachment', "Partially attached: {0}.", friendlyName); - } else { - ariaLabel = range ? localize('chat.fileAttachmentWithRange3', "Attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment3', "Attached: {0}.", friendlyName); - } + const renderFileAttachment = (ariaLabel: string, friendlyName: string, resource: URI, icon?: ThemeIcon) => { - if (attachment.isOmitted) { + if (attachment.omittedState === OmittedState.Full) { this.customAttachment(widget, friendlyName, hoverDelegate, ariaLabel, isAttachmentOmitted); } else { const fileOptions = { hidePath: true, title: correspondingContentReference?.options?.status?.description }; - label.setFile(resource, attachment.isFile ? { + label.setFile(resource, attachment.kind === 'file' ? { ...fileOptions, fileKind: FileKind.FILE, range, } : { ...fileOptions, fileKind: FileKind.FOLDER, - icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined + icon: icon || (!this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined) }); } @@ -128,28 +123,116 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.add(hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource)); } }); - } else if (attachment.isImage) { - ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); + }; + const renderImageAttachment = (ariaLabel: string, resource: URI | undefined, fullName: string, buffer: Uint8Array) => { const isURL = isImageVariableEntry(attachment) && attachment.isURL; - const hoverElement = this.customAttachment(widget, attachment.name, hoverDelegate, ariaLabel, isAttachmentOmitted, attachment.isImage, isURL, attachment.value as Uint8Array); + const hoverElement = this.customAttachment(widget, attachment.name, hoverDelegate, ariaLabel, isAttachmentOmitted, true, isURL, attachment.value as Uint8Array); - if (attachment.references) { + if (resource) { widget.style.cursor = 'pointer'; const clickHandler = () => { - if (attachment.references && URI.isUri(attachment.references[0].reference)) { - this.openResource(attachment.references[0].reference, false, undefined); - } + this.openResource(resource, false, undefined); }; this.attachedContextDisposables.add(dom.addDisposableListener(widget, 'click', clickHandler)); } + const omissionType = attachment.omittedState === OmittedState.Partial ? OmittedState.Partial : isAttachmentOmitted ? OmittedState.Full : undefined; + this.createImageElements(buffer, widget, hoverElement, fullName, resource, omissionType); + this.attachedContextDisposables.add(this.hoverService.setupDelayedHover(widget, { content: hoverElement, appearance: { showPointer: true } })); + widget.style.position = 'relative'; + }; + + const renderLabelWithIcon = (attachment: IChatRequestVariableEntry) => { + const attachmentLabel = attachment.fullName ?? attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, correspondingContentReference?.options?.status?.description); + }; + + if (resource && isNotebookOutputVariableEntry(attachment)) { + const friendlyName = attachment.name; + const output = this.getOutputItem(resource, attachment); + if (output?.mime.startsWith('image/')) { + if (attachment.omittedState === OmittedState.Full) { + ariaLabel = localize('chat.notebookOutputOmittedImageAttachment', "Omitted: {0}", friendlyName); + } else if (attachment.omittedState === OmittedState.Partial) { + ariaLabel = localize('chat.notebookOutputPartiallyOmittedImageAttachment', "Partially omitted: {0}", friendlyName); + } else { + ariaLabel = localize('chat.notebookOutputImageAttachment', "Attached: {0}", friendlyName); + } + } else { + if (isAttachmentOmitted) { + ariaLabel = localize('chat.notebookOutputOmittedFileAttachment', "Omitted: {0}.", friendlyName); + } else if (isAttachmentPartialOrOmitted) { + ariaLabel = localize('chat.notebookOutputPartialFileAttachment', "Partially attached: {0}.", friendlyName); + } else { + ariaLabel = localize('chat.notebookOutputFileAttachment3', "Attached: {0}.", friendlyName); + } + } - if (!isAttachmentPartialOrOmitted) { - const buffer = attachment.value as Uint8Array; - this.createImageElements(buffer, widget, hoverElement); - this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); + switch (output?.mime) { + case 'application/vnd.code.notebook.error': { + renderLabelWithIcon(attachment); + break; + } + case 'image/png': + case 'image/jpeg': + case 'image/svg': { + renderImageAttachment(ariaLabel, resource, attachment.name, output.data.buffer); + break; + } + default: { + renderFileAttachment(ariaLabel, attachment.name, resource, ThemeIcon.fromId('output')); + } } - widget.style.position = 'relative'; + + this.instantiationService.invokeFunction(accessor => { + if (resource) { + this.attachedContextDisposables.add(hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource)); + } + }); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { + const fileBasename = basename(resource.path); + const fileDirname = dirname(resource.path); + const friendlyName = `${fileBasename} ${fileDirname}`; + + if (isAttachmentOmitted) { + ariaLabel = range ? localize('chat.omittedFileAttachmentWithRange', "Omitted: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.omittedFileAttachment', "Omitted: {0}.", friendlyName); + } else if (isAttachmentPartialOrOmitted) { + ariaLabel = range ? localize('chat.partialFileAttachmentWithRange', "Partially attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.partialFileAttachment', "Partially attached: {0}.", friendlyName); + } else { + ariaLabel = range ? localize('chat.fileAttachmentWithRange3', "Attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment3', "Attached: {0}.", friendlyName); + } + + renderFileAttachment(ariaLabel, friendlyName, resource); + } else if (isImageVariableEntry(attachment)) { + if (attachment.omittedState === OmittedState.Full) { + ariaLabel = localize('chat.omittedImageAttachment', "Omitted this image: {0}", attachment.name); + } else if (attachment.omittedState === OmittedState.Partial) { + ariaLabel = localize('chat.partiallyOmittedImageAttachment', "Partially omitted this image: {0}", attachment.name); + } else { + ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); + } + + const ref = attachment.references?.[0]?.reference; + const resource = ref && URI.isUri(ref) ? ref : undefined; + renderImageAttachment(ariaLabel, resource, resource?.toString() ?? '', attachment.value as Uint8Array); + } else if (isElementVariableEntry(attachment)) { + ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name); + widget.style.cursor = 'pointer'; + const attachmentLabel = attachment.name; + const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel; + label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) }); + + this._register(dom.addDisposableListener(widget, dom.EventType.CLICK, async () => { + const content = attachment.value?.toString() || ''; + await this.editorService.openEditor({ + resource: undefined, + contents: content, + options: { + pinned: true + } + }); + })); } else if (isPasteVariableEntry(attachment)) { ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); @@ -182,11 +265,8 @@ export class ChatAttachmentsContentPart extends Disposable { } } } else { - const attachmentLabel = attachment.fullName ?? attachment.name; - const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; - label.setLabel(withIcon, correspondingContentReference?.options?.status?.description); - ariaLabel = localize('chat.attachment3', "Attached context: {0}.", attachment.name); + renderLabelWithIcon(attachment); } if (attachment.kind === 'symbol') { @@ -217,7 +297,7 @@ export class ChatAttachmentsContentPart extends Disposable { if (!this.attachedContextDisposables.isDisposed) { this.attachedContextDisposables.add(dom.addDisposableListener(widget, dom.EventType.CLICK, async (e: MouseEvent) => { dom.EventHelper.stop(e, true); - if (attachment.isDirectory) { + if (attachment.kind === 'directory') { this.openResource(resource, true); } else { this.openResource(resource, false, range); @@ -231,6 +311,23 @@ export class ChatAttachmentsContentPart extends Disposable { }); } + private getOutputItem(resource: URI, attachment: INotebookOutputVariableEntry) { + const parsedInfo = CellUri.parseCellOutputUri(resource); + if (!parsedInfo || typeof parsedInfo.cellHandle !== 'number' || typeof parsedInfo.outputIndex !== 'number') { + return undefined; + } + const notebook = this.notebookService.getNotebookTextModel(parsedInfo.notebook); + if (!notebook) { + return undefined; + } + const cell = notebook.cells.find(c => c.handle === parsedInfo.cellHandle); + if (!cell) { + return undefined; + } + const output = cell.outputs.length > parsedInfo.outputIndex ? cell.outputs[parsedInfo.outputIndex] : undefined; + return output?.outputs.find(o => o.mime === attachment.mimeType); + } + private customAttachment(widget: HTMLElement, friendlyName: string, hoverDelegate: IHoverDelegate, ariaLabel: string, isAttachmentOmitted: boolean, isImage?: boolean, isURL?: boolean, value?: Uint8Array): HTMLElement { const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(isAttachmentOmitted ? 'span.codicon.codicon-warning' : 'span.codicon.codicon-file-media')); const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName); @@ -242,14 +339,13 @@ export class ChatAttachmentsContentPart extends Disposable { if (isURL && !isAttachmentOmitted && value) { hoverElement.textContent = localize('chat.imageAttachmentHover', "{0}", convertUint8ArrayToString(value)); - this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + this.attachedContextDisposables.add(this.hoverService.setupDelayedHover(widget, { content: hoverElement, appearance: { showPointer: true } })); } - if (isAttachmentOmitted) { widget.classList.add('warning'); hoverElement.textContent = localize('chat.fileAttachmentHover', "Selected model does not support this {0} type.", isImage ? 'image' : 'file'); - this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); + this.attachedContextDisposables.add(this.hoverService.setupDelayedHover(widget, { content: hoverElement, appearance: { showPointer: true } })); } return hoverElement; @@ -274,10 +370,17 @@ export class ChatAttachmentsContentPart extends Disposable { } // Helper function to create and replace image - private async createImageElements(buffer: ArrayBuffer | Uint8Array, widget: HTMLElement, hoverElement: HTMLElement) { + private createImageElements(buffer: ArrayBuffer | Uint8Array, widget: HTMLElement, hoverElement: HTMLElement, fullName: string, reference?: URI, omittedState?: OmittedState): void { + if (omittedState === OmittedState.Full) { + return; + } + + if (omittedState === OmittedState.Partial) { + widget.classList.add('partial-warning'); + } + const blob = new Blob([buffer], { type: 'image/png' }); const url = URL.createObjectURL(blob); - const img = dom.$('img.chat-attached-context-image', { src: url, alt: '' }); const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' }); const pill = dom.$('div.chat-attached-context-pill', {}, pillImg); @@ -286,14 +389,22 @@ export class ChatAttachmentsContentPart extends Disposable { existingPill.replaceWith(pill); } - // Update hover image - hoverElement.appendChild(img); + const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' }); + const imageContainer = dom.$('div.chat-attached-context-image-container', {}, hoverImage); + hoverElement.appendChild(imageContainer); + + if (reference) { + const urlContainer = dom.$('a.chat-attached-context-url', {}, omittedState === OmittedState.Partial ? localize('chat.imageAttachmentWarning', "This GIF was partially omitted - current frame was be sent.") : fullName); + const separator = dom.$('div.chat-attached-context-url-separator'); + this._register(dom.addDisposableListener(urlContainer, 'click', () => this.openResource(reference, false, undefined))); + hoverElement.append(separator, urlContainer); + } - img.onload = () => { + hoverImage.onload = () => { URL.revokeObjectURL(url); }; - img.onerror = () => { + hoverImage.onerror = () => { // reset to original icon on error or invalid image const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts index b78f30375ed..976b3b74674 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts @@ -8,17 +8,12 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; -import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js'; +import { ChatTreeItem } from '../chat.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { $ } from './chatReferencesContentPart.js'; -import { EditorPool } from './chatMarkdownContentPart.js'; -import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions } from '../codeBlockPart.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IDisposableReference } from './chatCollections.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { autorun, IObservable, observableValue } from '../../../../../base/common/observable.js'; export abstract class ChatCollapsibleContentPart extends Disposable implements IChatContentPart { @@ -110,75 +105,3 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I this._isExpanded.set(value, undefined); } } - - -export class ChatCollapsibleEditorContentPart extends ChatCollapsibleContentPart { - - private readonly _editorReference: IDisposableReference; - private readonly _contentDomNode: HTMLElement; - - private _currentWidth: number = 0; - - readonly codeblocks: IChatCodeBlockInfo[] = []; - - constructor( - title: IMarkdownString | string, - context: IChatContentPartRenderContext, - private readonly editorPool: EditorPool, - private readonly textModel: Promise, - private readonly languageId: string, - private readonly options: ICodeBlockRenderOptions = {}, - private readonly codeBlockInfo: IChatCodeBlockInfo, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - super(title, context); - this._contentDomNode = $('div.chat-collapsible-editor-content'); - this._editorReference = this.editorPool.get(); - this.codeblocks = [{ - ...codeBlockInfo, - focus: () => { - this._editorReference.object.focus(); - codeBlockInfo.focus(); - } - }]; - } - - override dispose(): void { - this._editorReference?.dispose(); - super.dispose(); - } - - protected initContent(): HTMLElement { - const data: ICodeBlockData = { - languageId: this.languageId, - textModel: this.textModel, - codeBlockIndex: this.codeBlockInfo.codeBlockIndex, - codeBlockPartIndex: 0, - element: this.context.element, - parentContextKeyService: this.contextKeyService, - renderOptions: this.options - }; - - this._editorReference.object.render(data, this._currentWidth || 300); - this._register(this._editorReference.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); - this._contentDomNode.appendChild(this._editorReference.object.element); - - this._register(autorun(r => { - const value = this._isExpanded.read(r); - this._contentDomNode.style.display = value ? 'block' : 'none'; - })); - - - return this._contentDomNode; - } - - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - // For now, we consider content different unless it's exactly the same instance - return false; - } - - layout(width: number): void { - this._currentWidth = width; - this._editorReference.object.layout(width); - } -} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts index c3361ccfd3b..1019e32cfc6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts @@ -39,7 +39,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont { label: localize('accept', "Accept"), data: confirmation.data }, { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, ]; - const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, undefined, confirmation.message, buttons)); confirmationWidget.setShowButtons(!confirmation.isUsed); this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index 824fcd43c96..2298b1dd4f4 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -4,20 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; -import './media/chatConfirmationWidget.css'; import { Button, ButtonWithDropdown, IButton, IButtonOptions } from '../../../../../base/browser/ui/button/button.js'; +import { Action } from '../../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { IMarkdownRenderResult, MarkdownRenderer, openLinkFromMarkdown } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { autorun, observableValue } from '../../../../../base/common/observable.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { Action } from '../../../../../base/common/actions.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../../services/host/browser/host.js'; +import './media/chatConfirmationWidget.css'; export interface IChatConfirmationButton { label: string; @@ -27,6 +26,68 @@ export interface IChatConfirmationButton { moreActions?: IChatConfirmationButton[]; } +export class ChatQueryTitlePart extends Disposable { + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + private readonly _renderedTitle = this._register(new MutableDisposable()); + + public get title() { + return this._title; + } + + public set title(value: string | IMarkdownString) { + this._title = value; + + const next = this._renderer.render(this.toMdString(value), { + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + }); + + const previousEl = this._renderedTitle.value?.element; + if (previousEl?.parentElement) { + previousEl.parentElement.replaceChild(next.element, previousEl); + } else { + this.element.appendChild(next.element); // unreachable? + } + + this._renderedTitle.value = next; + } + + constructor( + private readonly element: HTMLElement, + private _title: IMarkdownString | string, + subtitle: string | IMarkdownString | undefined, + private readonly _renderer: MarkdownRenderer, + @IOpenerService private readonly _openerService: IOpenerService, + ) { + super(); + + element.classList.add('chat-query-title-part'); + + this._renderedTitle.value = _renderer.render(this.toMdString(_title), { + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + }); + element.append(this._renderedTitle.value.element); + if (subtitle) { + const str = this.toMdString(subtitle); + const renderedTitle = this._register(_renderer.render(str, { + asyncRenderCallback: () => this._onDidChangeHeight.fire(), + actionHandler: { callback: link => openLinkFromMarkdown(this._openerService, link, str.isTrusted), disposables: this._store }, + })); + const wrapper = document.createElement('small'); + wrapper.appendChild(renderedTitle.element); + element.append(wrapper); + } + } + + private toMdString(value: string | IMarkdownString) { + if (typeof value === 'string') { + return new MarkdownString('', { supportThemeIcons: true }).appendText(value); + } else { + return new MarkdownString(value.value, { supportThemeIcons: true, isTrusted: value.isTrusted }); + } + } +} + abstract class BaseChatConfirmationWidget extends Disposable { private _onDidClick = this._register(new Emitter()); get onDidClick(): Event { return this._onDidClick.event; } @@ -48,8 +109,8 @@ abstract class BaseChatConfirmationWidget extends Disposable { constructor( title: string, + subtitle: string | IMarkdownString | undefined, buttons: IChatConfirmationButton[], - expandableMessage: boolean, @IInstantiationService protected readonly instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -58,7 +119,6 @@ abstract class BaseChatConfirmationWidget extends Disposable { super(); const elements = dom.h('.chat-confirmation-widget@root', [ - dom.h('.chat-confirmation-widget-expando@expando'), dom.h('.chat-confirmation-widget-title@title'), dom.h('.chat-confirmation-widget-message@message'), dom.h('.chat-confirmation-buttons-container@buttonsContainer'), @@ -66,27 +126,16 @@ abstract class BaseChatConfirmationWidget extends Disposable { this._domNode = elements.root; this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - if (expandableMessage) { - const expanded = observableValue(this, false); - const btn = this._register(new Button(elements.expando, {})); - - this._register(autorun(r => { - const value = expanded.read(r); - btn.icon = value ? Codicon.chevronDown : Codicon.chevronRight; - elements.message.classList.toggle('hidden', !value); - this._onDidChangeHeight.fire(); - })); + const titlePart = this._register(instantiationService.createInstance( + ChatQueryTitlePart, + elements.title, + title, + subtitle, + this.markdownRenderer, + )); - this._register(btn.onDidClick(() => { - const value = expanded.get(); - expanded.set(!value, undefined); - })); - } + this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); - const renderedTitle = this._register(this.markdownRenderer.render(new MarkdownString(title, { supportThemeIcons: true }), { - asyncRenderCallback: () => this._onDidChangeHeight.fire(), - })); - elements.title.append(renderedTitle.element); this.messageElement = elements.message; buttons.forEach(buttonData => { const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip }; @@ -133,6 +182,7 @@ abstract class BaseChatConfirmationWidget extends Disposable { export class ChatConfirmationWidget extends BaseChatConfirmationWidget { constructor( title: string, + subtitle: string | IMarkdownString | undefined, private readonly message: string | IMarkdownString, buttons: IChatConfirmationButton[], @IInstantiationService instantiationService: IInstantiationService, @@ -140,7 +190,7 @@ export class ChatConfirmationWidget extends BaseChatConfirmationWidget { @IConfigurationService configurationService: IConfigurationService, @IHostService hostService: IHostService, ) { - super(title, buttons, false, instantiationService, contextMenuService, configurationService, hostService); + super(title, subtitle, buttons, instantiationService, contextMenuService, configurationService, hostService); const renderedMessage = this._register(this.markdownRenderer.render( typeof this.message === 'string' ? new MarkdownString(this.message) : this.message, @@ -153,15 +203,15 @@ export class ChatConfirmationWidget extends BaseChatConfirmationWidget { export class ChatCustomConfirmationWidget extends BaseChatConfirmationWidget { constructor( title: string, + subtitle: string | IMarkdownString | undefined, messageElement: HTMLElement, - messageElementIsExpandable: boolean, buttons: IChatConfirmationButton[], @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IHostService hostService: IHostService, ) { - super(title, buttons, messageElementIsExpandable, instantiationService, contextMenuService, configurationService, hostService); + super(title, subtitle, buttons, instantiationService, contextMenuService, configurationService, hostService); this.renderMessage(messageElement); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts new file mode 100644 index 00000000000..c042eecfc03 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatExtensionsContent.css'; +import * as dom from '../../../../../base/browser/dom.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ExtensionsList, getExtensions } from '../../../extensions/browser/extensionsViewer.js'; +import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; +import { IChatExtensionsContent } from '../../common/chatService.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; +import { ChatTreeItem, ChatViewId, IChatCodeBlockInfo } from '../chat.js'; +import { IChatContentPart } from './chatContentParts.js'; +import { PagedModel } from '../../../../../base/common/paging.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; + +export class ChatExtensionsContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public get codeblocks(): IChatCodeBlockInfo[] { + return []; + } + + public get codeblocksPartId(): string | undefined { + return undefined; + } + + constructor( + private readonly extensionsContent: IChatExtensionsContent, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this.domNode = dom.$('.chat-extensions-content-part'); + const loadingElement = dom.append(this.domNode, dom.$('.loading-extensions-element')); + dom.append(loadingElement, dom.$(ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin'))), dom.$('span.loading-message', undefined, localize('chat.extensions.loading', 'Loading extensions...'))); + + const extensionsList = dom.append(this.domNode, dom.$('.extensions-list')); + const list = this._register(instantiationService.createInstance(ExtensionsList, extensionsList, ChatViewId, { alwaysConsumeMouseWheel: false }, { onFocus: Event.None, onBlur: Event.None, filters: {} })); + getExtensions(extensionsContent.extensions, extensionsWorkbenchService).then(extensions => { + loadingElement.remove(); + if (this._store.isDisposed) { + return; + } + list.setModel(new PagedModel(extensions)); + list.layout(); + this._onDidChangeHeight.fire(); + }); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'extensions' && other.extensions.length === this.extensionsContent.extensions.length && other.extensions.every(ext => this.extensionsContent.extensions.includes(ext)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index b939a1ad578..295e2a0223a 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -45,6 +45,7 @@ import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions, localFileLangua import '../media/chatCodeBlockPill.css'; import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; +import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; const $ = dom.$; @@ -105,6 +106,11 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP hideEmptyCodeblock.style.display = 'none'; return hideEmptyCodeblock; } + if (languageId === 'vscode-extensions') { + const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); + this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + return chatExtensions.domNode; + } const globalIndex = globalCodeBlockIndexStart++; const thisPartIndex = thisPartCodeBlockIndexStart++; let textModel: Promise; @@ -135,7 +141,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP if (hideToolbar !== undefined) { renderOptions.hideToolbar = hideToolbar; } - const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions }; + const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions, chatSessionId: element.sessionId }; if (element.isCompleteAddedRequest || !codeblockEntry?.codemapperUri || !codeblockEntry.isEdit) { const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); @@ -151,6 +157,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP readonly codeBlockIndex = globalIndex; readonly elementId = element.id; readonly isStreaming = false; + readonly chatSessionId = element.sessionId; codemapperUri = undefined; // will be set async public get uri() { // here we must do a getter because the ref.object is rendered @@ -184,6 +191,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP readonly elementId = element.id; readonly isStreaming = !isCodeBlockComplete; readonly codemapperUri = codeblockEntry?.codemapperUri; + readonly chatSessionId = element.sessionId; public get uri() { return undefined; } @@ -418,7 +426,8 @@ class CollapsedCodeBlock extends Disposable { } modifiedByResponse = modifiedEntry?.isCurrentlyBeingModifiedBy.read(r); - const isComplete = !modifiedByResponse || modifiedByResponse.requestId !== this.requestId; + let diffValue = diffBetweenStops?.read(r); + const isComplete = !!diffValue || !modifiedByResponse || modifiedByResponse.requestId !== this.requestId; const rewriteRatio = modifiedEntry?.rewriteRatio.read(r); if (!isStreaming && !isComplete) { @@ -435,10 +444,11 @@ class CollapsedCodeBlock extends Disposable { diffBetweenStops = modifiedEntry && editSession ? editSession.getEntryDiffBetweenStops(modifiedEntry.modifiedURI, this.requestId, this.inUndoStop) : undefined; + diffValue = diffBetweenStops?.read(r); } - if (!isStreaming && isComplete && diffBetweenStops) { - renderDiff(diffBetweenStops.read(r)); + if (!isStreaming && isComplete) { + renderDiff(diffValue); } })); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts index c0017f21f84..6c5c13e045e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append } from '../../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType } from '../../../../../base/browser/dom.js'; import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -111,6 +111,14 @@ export class ChatWorkingProgressContentPart extends ChatProgressContentPart impl new MarkdownString().appendText(localize('workingMessage', "Working...")) }; super(progressMessage, renderer, context, undefined, undefined, workingProgress.isPaused ? Codicon.debugPause : undefined, instantiationService, chatMarkdownAnchorService); + + if (workingProgress.isPaused) { + this.domNode.style.cursor = 'pointer'; + this.domNode.title = localize('resume', "Click to resume"); + this._register(addDisposableListener(this.domNode, EventType.CLICK, () => { + workingProgress.setPaused(false); + })); + } } override hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts index e41a5813ab0..fc645564d61 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts @@ -18,6 +18,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { asCssVariable, textLinkForeground } from '../../../../../platform/theme/common/colorRegistry.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js'; import { IChatResponseViewModel } from '../../common/chatViewModel.js'; import { IChatWidgetService } from '../chat.js'; import { IChatContentPart } from './chatContentParts.js'; @@ -46,6 +47,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar @IChatWidgetService chatWidgetService: IChatWidgetService, @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService ) { super(); @@ -60,9 +62,18 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar const markdownContent = renderer.render(new MarkdownString(errorDetails.message)); dom.append(messageContainer, markdownContent.element); - const button1 = this._register(new Button(messageContainer, { ...defaultButtonStyles, supportIcons: true })); - button1.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); - button1.element.classList.add('chat-quota-error-button'); + let button1Label = ''; + switch (chatEntitlementService.entitlement) { + case ChatEntitlement.Pro: + case ChatEntitlement.ProPlus: + button1Label = localize('enableAdditionalUsage', "Manage paid premium requests"); + break; + case ChatEntitlement.Limited: + button1Label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); + break; + default: + button1Label = ''; + } let hasAddedWaitWarning = false; const addWaitWarningIfNeeded = () => { @@ -71,7 +82,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar } hasAddedWaitWarning = true; - dom.append(messageContainer, $('.chat-quota-wait-warning', undefined, localize('waitWarning', "Signing up may take a few minutes to take effect."))); + dom.append(messageContainer, $('.chat-quota-wait-warning', undefined, localize('waitWarning', "Changes may take a few minutes to take effect."))); }; let hasAddedRetryButton = false; @@ -86,7 +97,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar buttonForeground: asCssVariable(textLinkForeground) })); button2.element.classList.add('chat-quota-error-secondary-button'); - button2.label = localize('signedUpClickToContinue', "Signed up? Click to retry."); + button2.label = localize('clickToContinue', "Click to retry."); this._onDidChangeHeight.fire(); this._register(button2.onDidClick(() => { const widget = chatWidgetService.getWidgetBySessionId(element.sessionId); @@ -101,14 +112,19 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar })); }; - this._register(button1.onDidClick(async () => { - const commandId = 'workbench.action.chat.upgradePlan'; - telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-response' }); - await commandService.executeCommand(commandId); - - shouldShowRetryButton = true; - addRetryButtonIfNeeded(); - })); + if (button1Label) { + const button1 = this._register(new Button(messageContainer, { ...defaultButtonStyles, supportIcons: true })); + button1.label = button1Label; + button1.element.classList.add('chat-quota-error-button'); + this._register(button1.onDidClick(async () => { + const commandId = chatEntitlementService.entitlement === ChatEntitlement.Limited ? 'workbench.action.chat.upgradePlan' : 'workbench.action.chat.manageOverages'; + telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-response' }); + await commandService.executeCommand(commandId); + + shouldShowRetryButton = true; + addRetryButtonIfNeeded(); + })); + } addRetryButtonIfNeeded(); addWaitWarningIfNeeded(); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 3d5115ef8cd..291f81a0c8c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -37,9 +37,8 @@ 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'; import { ChatTreeItem, IChatWidgetService } from '../chat.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; @@ -51,7 +50,7 @@ export const $ = dom.$; export interface IChatReferenceListItem extends IChatContentReference { title?: string; description?: string; - state?: WorkingSetEntryState; + state?: ModifiedFileEntryState; excluded?: boolean; } @@ -402,10 +401,10 @@ class CollapsibleListRenderer implements IListRenderer { const chatWidgetService = accessor.get(IChatWidgetService); - const variablesService = accessor.get(IChatVariablesService); - if (!resource) { return; } const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - return; + if (widget) { + widget.attachmentModel.addFile(resource); } - - variablesService.attachContext('file', resource, widget.location); } }); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts index 65f1ab01e16..310130ec951 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts @@ -18,6 +18,8 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart public readonly domNode: HTMLElement; public readonly onDidChangeHeight: Event; + private isSettled: boolean; + constructor( private readonly task: IChatTask, contentReferencesListPool: CollapsibleListPool, @@ -28,6 +30,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart super(); if (task.progress.length) { + this.isSettled = true; const refsPart = this._register(instantiationService.createInstance(ChatCollapsibleListContentPart, task.progress, task.content.value, context, contentReferencesListPool)); this.domNode = dom.$('.chat-progress-task'); this.domNode.appendChild(refsPart.domNode); @@ -35,6 +38,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart } else { // #217645 const isSettled = task.isSettled?.() ?? true; + this.isSettled = isSettled; const showSpinner = !isSettled && !context.element.isComplete; const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, showSpinner, true, undefined)); this.domNode = progressPart.domNode; @@ -45,7 +49,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart hasSameContent(other: IChatProgressRenderableResponseContent): boolean { return other.kind === 'progressTask' && other.progress.length === this.task.progress.length - && other.isSettled() === this.task.isSettled(); + && other.isSettled() === this.isSettled; } addDisposable(disposable: IDisposable): void { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts new file mode 100644 index 00000000000..b354718cfbb --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { localize } from '../../../../../nls.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; +import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js'; +import { getAttachableImageExtension } from '../chatAttachmentResolve.js'; +import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions } from '../codeBlockPart.js'; +import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js'; +import { IDisposableReference } from './chatCollections.js'; +import { ChatQueryTitlePart } from './chatConfirmationWidget.js'; +import { IChatContentPartRenderContext } from './chatContentParts.js'; +import { EditorPool } from './chatMarkdownContentPart.js'; + +export interface IChatCollapsibleIOCodePart { + kind: 'code'; + textModel: ITextModel; + languageId: string; + options: ICodeBlockRenderOptions; + codeBlockInfo: IChatCodeBlockInfo; +} + +export interface IChatCollapsibleIODataPart { + kind: 'data'; + value: Uint8Array; + mimeType: string; +} + +export interface IChatCollapsibleInputData extends IChatCollapsibleIOCodePart { } +export interface IChatCollapsibleOutputData { + // todo: show images etc. here + parts: (IChatCollapsibleIOCodePart | IChatCollapsibleIODataPart)[]; +} + +export class ChatCollapsibleInputOutputContentPart extends Disposable { + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + private _currentWidth: number = 0; + private readonly _editorReferences: IDisposableReference[] = []; + private readonly _titlePart: ChatQueryTitlePart; + public readonly domNode: HTMLElement; + + readonly codeblocks: IChatCodeBlockInfo[] = []; + + public set title(s: string | IMarkdownString) { + this._titlePart.title = s; + } + + public get title(): string | IMarkdownString { + return this._titlePart.title; + } + + private readonly _expanded: ISettableObservable; + + public get expanded(): boolean { + return this._expanded.get(); + } + + constructor( + title: IMarkdownString | string, + subtitle: string | IMarkdownString | undefined, + private readonly context: IChatContentPartRenderContext, + private readonly editorPool: EditorPool, + private readonly input: IChatCollapsibleInputData, + private readonly output: IChatCollapsibleOutputData | undefined, + isError: boolean, + initiallyExpanded: boolean, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + const elements = dom.h('.chat-confirmation-widget@root', [ + dom.h('.chat-confirmation-widget-title.expandable@titleContainer', [ + dom.h('.chat-confirmation-widget-expando@expando'), + dom.h('.chat-confirmation-widget-title-inner@title'), + dom.h('.chat-confirmation-widget-title-icon@icon'), + ]), + dom.h('.chat-confirmation-widget-message@message'), + ]); + this.domNode = elements.root; + + const titlePart = this._titlePart = this._register(_instantiationService.createInstance( + ChatQueryTitlePart, + elements.title, + title, + subtitle, + _instantiationService.createInstance(MarkdownRenderer, {}), + )); + + this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + const spacer = document.createElement('span'); + spacer.style.flexGrow = '1'; + elements.title.appendChild(spacer); + const check = dom.h(isError + ? ThemeIcon.asCSSSelector(Codicon.error) + : output + ? ThemeIcon.asCSSSelector(Codicon.check) + : ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin')) + ); + elements.icon.appendChild(check.root); + + const expanded = this._expanded = observableValue(this, initiallyExpanded); + const btn = this._register(new Button(elements.expando, {})); + + this._register(autorun(r => { + const value = expanded.read(r); + btn.icon = value ? Codicon.chevronDown : Codicon.chevronRight; + elements.root.classList.toggle('collapsed', !value); + this._onDidChangeHeight.fire(); + })); + + const toggle = (e: Event) => { + if (!e.defaultPrevented) { + const value = expanded.get(); + expanded.set(!value, undefined); + e.preventDefault(); + } + }; + + this._register(btn.onDidClick(toggle)); + this._register(dom.addDisposableListener(elements.titleContainer, dom.EventType.CLICK, toggle)); + + elements.message.appendChild(this.createMessageContents()); + } + + private createMessageContents() { + const contents = dom.h('div', [ + dom.h('h3@inputTitle'), + dom.h('div@input'), + dom.h('h3@outputTitle'), + dom.h('div@output'), + ]); + + const { input, output } = this; + + contents.inputTitle.textContent = localize('chat.input', "Input"); + this.addCodeBlock(input, contents.input); + + if (!output) { + contents.output.remove(); + contents.outputTitle.remove(); + } else { + contents.outputTitle.textContent = localize('chat.output', "Output"); + for (const part of output.parts) { + if (part.kind === 'data' && getAttachableImageExtension(part.mimeType)) { + const n = this._register(this._instantiationService.createInstance( + ChatAttachmentsContentPart, + [{ kind: 'image', id: generateUuid(), name: `image.${getAttachableImageExtension(part.mimeType)}`, value: part.value, mimeType: part.mimeType, isURL: false }], + undefined, + undefined, + )); + contents.output.appendChild(n.domNode!); + } else if (part.kind === 'code') { + this.addCodeBlock(part, contents.output); + } + } + } + + return contents.root; + } + + private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) { + const data: ICodeBlockData = { + languageId: part.languageId, + textModel: Promise.resolve(part.textModel), + codeBlockIndex: part.codeBlockInfo.codeBlockIndex, + codeBlockPartIndex: 0, + element: this.context.element, + parentContextKeyService: this.contextKeyService, + renderOptions: part.options, + chatSessionId: this.context.element.sessionId, + }; + const editorReference = this._register(this.editorPool.get()); + editorReference.object.render(data, this._currentWidth || 300); + this._register(editorReference.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); + container.appendChild(editorReference.object.element); + this._editorReferences.push(editorReference); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + // For now, we consider content different unless it's exactly the same instance + return false; + } + + layout(width: number): void { + this._currentWidth = width; + this._editorReferences.forEach(r => r.object.layout(width)); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index 5c3ef465a0b..dca560d629c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -4,35 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { assertNever } from '../../../../../base/common/assert.js'; +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { decodeBase64 } from '../../../../../base/common/buffer.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorunWithStore } from '../../../../../base/common/observable.js'; +import { count } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { isEmptyObject } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { Location } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { localize } from '../../../../../nls.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; -import { createToolInputUri, ILanguageModelToolsService, isToolResultInputOutputDetails, IToolResultInputOutputDetails } from '../../common/languageModelToolsService.js'; +import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService, isToolResultInputOutputDetails, IToolResultInputOutputDetails } from '../../common/languageModelToolsService.js'; import { CancelChatActionId } from '../actions/chatExecuteActions.js'; import { AcceptToolConfirmationActionId } from '../actions/chatToolActions.js'; -import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js'; +import { ChatTreeItem, IChatCodeBlockInfo, IChatWidgetService } from '../chat.js'; +import { getAttachableImageExtension } from '../chatAttachmentResolve.js'; import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; -import { ChatCollapsibleEditorContentPart } from './chatCollapsibleContentPart.js'; import { ChatConfirmationWidget, ChatCustomConfirmationWidget, IChatConfirmationButton } from './chatConfirmationWidget.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatMarkdownContentPart, EditorPool } from './chatMarkdownContentPart.js'; import { ChatCustomProgressPart, ChatProgressContentPart } from './chatProgressContentPart.js'; import { ChatCollapsibleListContentPart, CollapsibleListPool, IChatCollapsibleListItem } from './chatReferencesContentPart.js'; +import { ChatCollapsibleInputOutputContentPart, IChatCollapsibleIOCodePart, IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js'; export class ChatToolInvocationPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -98,6 +108,9 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa class ChatToolInvocationSubPart extends Disposable { private static idPool = 0; + /** Remembers expanded tool parts on re-render */ + private static readonly _expandedByDefault = new WeakMap(); + private readonly _codeblocksPartId = 'tool-' + (ChatToolInvocationSubPart.idPool++); public readonly domNode: HTMLElement; @@ -133,7 +146,10 @@ class ChatToolInvocationSubPart extends Disposable { @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, + @ICommandService private readonly commandService: ICommandService, + @IMarkerService private readonly markerService: IMarkerService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); @@ -148,7 +164,9 @@ class ChatToolInvocationSubPart extends Disposable { } else if (Array.isArray(toolInvocation.resultDetails) && toolInvocation.resultDetails?.length) { this.domNode = this.createResultList(toolInvocation.pastTenseMessage ?? toolInvocation.invocationMessage, toolInvocation.resultDetails); } else if (isToolResultInputOutputDetails(toolInvocation.resultDetails)) { - this.domNode = this.createInputOutputMarkdownProgressPart(toolInvocation.pastTenseMessage ?? toolInvocation.invocationMessage, toolInvocation.resultDetails); + this.domNode = this.createInputOutputMarkdownProgressPart(toolInvocation.pastTenseMessage ?? toolInvocation.invocationMessage, toolInvocation.originMessage, toolInvocation.resultDetails.input, toolInvocation.resultDetails.output, !!toolInvocation.resultDetails.isError); + } else if (toolInvocation.kind === 'toolInvocation' && toolInvocation.toolSpecificData?.kind === 'input' && !toolInvocation.isComplete) { + this.domNode = this.createInputOutputMarkdownProgressPart(this.toolInvocation.invocationMessage, toolInvocation.originMessage, typeof toolInvocation.toolSpecificData.rawInput === 'string' ? toolInvocation.toolSpecificData.rawInput : JSON.stringify(toolInvocation.toolSpecificData.rawInput, null, 2), undefined, false); } else { this.domNode = this.createProgressPart(); } @@ -162,9 +180,7 @@ class ChatToolInvocationSubPart extends Disposable { if (!toolInvocation.confirmationMessages) { throw new Error('Confirmation messages are missing'); } - const title = toolInvocation.confirmationMessages.title; - const message = toolInvocation.confirmationMessages.message; - const allowAutoConfirm = toolInvocation.confirmationMessages.allowAutoConfirm; + const { title, message, allowAutoConfirm } = toolInvocation.confirmationMessages; const continueLabel = localize('continue', "Continue"); const continueKeybinding = this.keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel(); const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel; @@ -202,6 +218,7 @@ class ChatToolInvocationSubPart extends Disposable { confirmWidget = this._register(this.instantiationService.createInstance( ChatConfirmationWidget, title, + toolInvocation.originMessage, message, buttons )); @@ -224,7 +241,7 @@ class ChatToolInvocationSubPart extends Disposable { dom.h('.editor@editor'), ]); - if (toolInvocation.toolSpecificData?.kind === 'input') { + if (toolInvocation.toolSpecificData?.kind === 'input' && toolInvocation.toolSpecificData.rawInput && !isEmptyObject(toolInvocation.toolSpecificData.rawInput)) { const inputData = toolInvocation.toolSpecificData; @@ -234,17 +251,50 @@ class ChatToolInvocationSubPart extends Disposable { maxHeightInLines: 13, verticalPadding: 5, editorOptions: { - wordWrap: 'on', + wordWrap: 'off', readOnly: false } }; const langId = this.languageService.getLanguageIdByLanguageName('json'); + const rawJsonInput = JSON.stringify(inputData.rawInput ?? {}, null, 1); + const canSeeMore = count(rawJsonInput, '\n') > 2; // if more than one key:value const model = this._register(this.modelService.createModel( - JSON.stringify(inputData.rawInput, undefined, 2), + // View a single JSON line by default until they 'see more' + rawJsonInput.replace(/\n */g, ' '), this.languageService.createById(langId), createToolInputUri(toolInvocation.toolId) )); + + const markerOwner = generateUuid(); + const schemaUri = createToolSchemaUri(toolInvocation.toolId); + const validator = new RunOnceScheduler(async () => { + + const newMarker: IMarkerData[] = []; + + const result = await this.commandService.executeCommand('json.validate', schemaUri, model.getValue()); + for (const item of result) { + if (item.range && item.message) { + newMarker.push({ + severity: item.severity === 'Error' ? MarkerSeverity.Error : MarkerSeverity.Warning, + message: item.message, + startLineNumber: item.range[0].line + 1, + startColumn: item.range[0].character + 1, + endLineNumber: item.range[1].line + 1, + endColumn: item.range[1].character + 1, + code: item.code ? String(item.code) : undefined + }); + } + } + + this.markerService.changeOne(markerOwner, model.uri, newMarker); + }, 500); + + validator.schedule(); + this._register(model.onDidChangeContent(() => validator.schedule())); + this._register(toDisposable(() => this.markerService.remove(markerOwner, [model.uri]))); + this._register(validator); + const editor = this._register(this.editorPool.get()); editor.object.render({ codeBlockIndex: this.codeBlockStartIndex, @@ -252,7 +302,8 @@ class ChatToolInvocationSubPart extends Disposable { element: this.context.element, languageId: langId ?? 'json', renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model) + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId }, this.currentWidthDelegate()); this._codeblocks.push({ codeBlockIndex: this.codeBlockStartIndex, @@ -262,7 +313,8 @@ class ChatToolInvocationSubPart extends Disposable { isStreaming: false, ownerMarkdownPartId: this.codeblocksPartId, uri: model.uri, - uriPromise: Promise.resolve(model.uri) + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); @@ -277,6 +329,22 @@ class ChatToolInvocationSubPart extends Disposable { })); elements.editor.append(editor.object.element); + + if (canSeeMore) { + const seeMore = dom.h('div.see-more', [dom.h('a@link')]); + seeMore.link.textContent = localize('seeMore', "See more"); + this._register(dom.addDisposableGenericMouseDownListener(seeMore.link, () => { + try { + const parsed = JSON.parse(model.getValue()); + model.setValue(JSON.stringify(parsed, null, 2)); + editor.object.editor.updateOptions({ wordWrap: 'on' }); + } catch { + // ignored + } + seeMore.root.remove(); + })); + elements.editor.append(seeMore.root); + } } this.markdownPart = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, this.context, this.editorPool, false, this.codeBlockStartIndex, this.renderer, this.currentWidthDelegate(), this.codeBlockModelCollection, { codeBlockRenderOptions })); @@ -286,8 +354,8 @@ class ChatToolInvocationSubPart extends Disposable { confirmWidget = this._register(this.instantiationService.createInstance( ChatCustomConfirmationWidget, title, + toolInvocation.originMessage, elements.root, - toolInvocation.toolSpecificData?.kind === 'input', buttons )); } @@ -316,6 +384,8 @@ class ChatToolInvocationSubPart extends Disposable { toolInvocation.confirmed.complete(false); break; } + + this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); })); this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); this._register(toDisposable(() => hasToolConfirmation.reset())); @@ -367,14 +437,16 @@ class ChatToolInvocationSubPart extends Disposable { const langId = this.languageService.getLanguageIdByLanguageName(terminalData.language ?? 'sh') ?? 'shellscript'; const model = this.modelService.createModel(terminalData.command, this.languageService.createById(langId)); const editor = this._register(this.editorPool.get()); - editor.object.render({ + const renderPromise = editor.object.render({ codeBlockIndex: this.codeBlockStartIndex, codeBlockPartIndex: 0, element: this.context.element, languageId: langId, renderOptions: codeBlockRenderOptions, - textModel: Promise.resolve(model) + textModel: Promise.resolve(model), + chatSessionId: this.context.element.sessionId }, this.currentWidthDelegate()); + this._register(thenIfNotDisposed(renderPromise, () => this._onDidChangeHeight.fire())); this._codeblocks.push({ codeBlockIndex: this.codeBlockStartIndex, codemapperUri: undefined, @@ -383,7 +455,8 @@ class ChatToolInvocationSubPart extends Disposable { isStreaming: false, ownerMarkdownPartId: this.codeblocksPartId, uri: model.uri, - uriPromise: Promise.resolve(model.uri) + uriPromise: Promise.resolve(model.uri), + chatSessionId: this.context.element.sessionId }); this._register(editor.object.onDidChangeContentHeight(() => { editor.object.layout(this.currentWidthDelegate()); @@ -398,14 +471,15 @@ class ChatToolInvocationSubPart extends Disposable { const confirmWidget = this._register(this.instantiationService.createInstance( ChatCustomConfirmationWidget, title, + undefined, element, - false, buttons )); ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService).set(true); this._register(confirmWidget.onDidClick(button => { toolInvocation.confirmed.complete(button.data); + this.chatWidgetService.getWidgetBySessionId(this.context.element.sessionId)?.focusInput(); })); this._register(confirmWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); toolInvocation.confirmed.p.then(() => { @@ -416,27 +490,37 @@ class ChatToolInvocationSubPart extends Disposable { } private createProgressPart(): HTMLElement { - let content: IMarkdownString; if (this.toolInvocation.isComplete && this.toolInvocation.isConfirmed !== false && this.toolInvocation.pastTenseMessage) { - content = typeof this.toolInvocation.pastTenseMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.pastTenseMessage) : - this.toolInvocation.pastTenseMessage; + const part = this.renderProgressContent(this.toolInvocation.pastTenseMessage); + this._register(part); + return part.domNode; } else { - content = typeof this.toolInvocation.invocationMessage === 'string' ? - new MarkdownString().appendText(this.toolInvocation.invocationMessage + '…') : - MarkdownString.lift(this.toolInvocation.invocationMessage).appendText('…'); + const container = document.createElement('div'); + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; + this._register(autorunWithStore((reader, store) => { + const progress = progressObservable?.read(reader); + const part = store.add(this.renderProgressContent(progress?.message || this.toolInvocation.invocationMessage)); + dom.reset(container, part.domNode); + })); + return container; + } + } + + private renderProgressContent(content: IMarkdownString | string) { + if (typeof content === 'string') { + content = new MarkdownString().appendText(content); } const progressMessage: IChatProgressMessage = { kind: 'progressMessage', content }; + const iconOverride = !this.toolInvocation.isConfirmed ? Codicon.error : this.toolInvocation.isComplete ? Codicon.check : undefined; - const progressPart = this._register(this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride)); - return progressPart.domNode; + return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, iconOverride); } private createTerminalMarkdownProgressPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, terminalData: IChatTerminalToolInvocationData): HTMLElement { @@ -464,41 +548,85 @@ class ChatToolInvocationSubPart extends Disposable { return progressPart.domNode; } - private createInputOutputMarkdownProgressPart(message: string | IMarkdownString, inputOutputData: IToolResultInputOutputDetails): HTMLElement { + private createInputOutputMarkdownProgressPart(message: string | IMarkdownString, subtitle: string | IMarkdownString | undefined, input: string, output: IToolResultInputOutputDetails['output'] | undefined, isError: boolean): HTMLElement { + let codeBlockIndex = this.codeBlockStartIndex; + const toCodePart = (data: string): IChatCollapsibleIOCodePart => { + const model = this._register(this.modelService.createModel( + data, + this.languageService.createById('json') + )); - const model = this._register(this.modelService.createModel( - `${inputOutputData.input}\n\n${inputOutputData.output}`, - this.languageService.createById('json') - )); + return { + kind: 'code', + textModel: model, + languageId: model.getLanguageId(), + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on' + } + }, + codeBlockInfo: { + codeBlockIndex: codeBlockIndex++, + codemapperUri: undefined, + elementId: this.context.element.id, + focus: () => { }, + isStreaming: false, + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + chatSessionId: this.context.element.sessionId, + uriPromise: Promise.resolve(model.uri) + } + }; + }; + + if (typeof output === 'string') { // back compat with older stored versions + output = [{ type: 'text', value: output }]; + } const collapsibleListPart = this._register(this.instantiationService.createInstance( - ChatCollapsibleEditorContentPart, + ChatCollapsibleInputOutputContentPart, message, + subtitle, this.context, this.editorPool, - Promise.resolve(model), - model.getLanguageId(), - { - hideToolbar: true, - reserveWidth: 19, - maxHeightInLines: 13, - verticalPadding: 5, - editorOptions: { - wordWrap: 'on' - } + toCodePart(input), + output && { + parts: output.map((o): IChatCollapsibleIODataPart | IChatCollapsibleIOCodePart => { + if (o.type === 'data') { + const decoded = decodeBase64(o.value64).buffer; + if (getAttachableImageExtension(o.mimeType)) { + return { kind: 'data', value: decoded, mimeType: o.mimeType }; + } else { + return toCodePart(localize('toolResultData', "Data of type {0} ({1} bytes)", o.mimeType, decoded.byteLength)); + } + } else if (o.type === 'text') { + return toCodePart(o.value); + } else { + assertNever(o); + } + }), }, - { - codeBlockIndex: this.codeBlockStartIndex, - codemapperUri: undefined, - elementId: this.context.element.id, - focus: () => { }, - isStreaming: false, - ownerMarkdownPartId: this.codeblocksPartId, - uri: model.uri, - uriPromise: Promise.resolve(model.uri) - } + isError, + ChatToolInvocationSubPart._expandedByDefault.get(this.toolInvocation) ?? false, )); + this._codeblocks.push(...collapsibleListPart.codeblocks); this._register(collapsibleListPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this._register(toDisposable(() => ChatToolInvocationSubPart._expandedByDefault.set(this.toolInvocation, collapsibleListPart.expanded))); + + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.progress : undefined; + if (progressObservable) { + this._register(autorunWithStore((reader, store) => { + const progress = progressObservable?.read(reader); + if (progress.message) { + collapsibleListPart.title = progress.message; + } + })); + } + return collapsibleListPart.domNode; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css index 94e2f791cc7..76f390053ac 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css @@ -6,7 +6,7 @@ .chat-confirmation-widget { border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; - padding: 8px 12px 12px; + padding: 3px; display: flex; flex-wrap: wrap; align-items: center; @@ -16,20 +16,86 @@ margin-bottom: 16px; } -.chat-confirmation-widget .chat-confirmation-widget-title { - font-weight: 600; +.chat-confirmation-widget .chat-confirmation-widget-title { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 100%; + border-radius: 3px; + padding: 3px 8px; + user-select: none; + gap: 4px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title.expandable { + cursor: pointer; + margin-left: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-title.expandable:hover { + background: var(--vscode-toolbar-hoverBackground); +} + +.chat-confirmation-widget .chat-confirmation-widget-title-inner { + flex-grow: 1; + flex-basis: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-title-icon { + line-height: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p, +.chat-confirmation-widget .chat-confirmation-widget-title .rendered-markdown { + display: inline; } .chat-confirmation-widget .chat-confirmation-widget-title p { - margin: 0 0 4px 0; + margin: 0 !important; +} +.chat-confirmation-widget .chat-confirmation-widget-title .codicon-check { + color: var(--vscode-debugIcon-startForeground) !important; +} +.chat-confirmation-widget .chat-confirmation-widget-title .codicon-error { + color: var(--vscode-errorForeground) !important; +} + +.chat-confirmation-widget .chat-confirmation-widget-title .chat-confirmation-widget-expando { + display: flex; + align-items: center; +} + +.chat-confirmation-widget-message h3 { + font-weight: 600; + margin: 4px 0 8px; + font-size: 14px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title .rendered-markdown p a { + color: inherit; +} + +.chat-confirmation-widget-title small { + font-size: 1em; + opacity: 0.85; + + &::before { + content: ' \2013 '; + } } .chat-confirmation-widget .chat-confirmation-buttons-container, .chat-confirmation-widget .chat-confirmation-widget-message { flex-basis: 100%; + padding: 0 8px; + margin: 8px 0; + + &:last-child { + margin-bottom: 0; + } } -.chat-confirmation-widget .chat-confirmation-widget-message.hidden { +.chat-confirmation-widget.collapsed .chat-confirmation-widget-message { display: none; } @@ -41,11 +107,26 @@ margin-bottom: 0px; } +.chat-confirmation-widget .chat-confirmation-widget-message .see-more { + margin-top: -4px; + + a { + color: var(--vscode-textLink-foreground); + text-decoration: underline; + display: block; + cursor: pointer; + } +} + .chat-confirmation-widget .chat-confirmation-buttons-container { display: flex; gap: 8px; - margin-top: 13px; + margin-top: 0px; flex-wrap: wrap; + + &:last-child { + margin-bottom: 8px; + } } .chat-confirmation-widget.hideButtons .chat-confirmation-buttons-container { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css new file mode 100644 index 00000000000..f9be266abd3 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-extensions-content-part { + border: 1px solid var(--vscode-chat-requestBorder); + border-bottom: none; + border-radius: 4px; +} + +.chat-extensions-content-part .extension-list-item { + border-bottom: 1px solid var(--vscode-chat-requestBorder); +} + +.chat-extensions-content-part .loading-extensions-element { + line-height: 18px; + padding: 4px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + user-select: none; + border-bottom: 1px solid var(--vscode-chat-requestBorder); +} + +.chat-extensions-content-part .loading-extensions-element .loading-message { + padding-left: 4px; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index ba80693ce0c..e3f054946e5 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -12,30 +12,23 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { UriList } from '../../../../base/common/dataTransfer.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; -import { basename } from '../../../../base/common/resources.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 { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractMarkerDropData, extractSymbolDropData, IDraggedResourceEditorInput, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js'; +import { CodeDataTransfers, containsDragType, extractEditorsDropData, extractMarkerDropData, extractNotebookCellOutputDropData, extractSymbolDropData } from '../../../../platform/dnd/browser/dnd.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.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 { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IChatWidgetService } from './chat.js'; +import { ImageTransferData, resolveEditorAttachContext, resolveImageAttachContext, resolveMarkerAttachContext, resolveNotebookOutputAttachContext, resolveSymbolsAttachContext } from './chatAttachmentResolve.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; -import { imageToHash } from './chatPasteProviders.js'; -import { resizeImage } from './imageUtils.js'; +import { convertStringToUInt8Array } from './imageUtils.js'; enum ChatDragAndDropType { FILE_INTERNAL, @@ -45,8 +38,12 @@ enum ChatDragAndDropType { SYMBOL, HTML, MARKER, + NOTEBOOK_CELL_OUTPUT } +const IMAGE_DATA_REGEX = /^data:image\/[a-z]+;base64,/; +const URL_REGEX = /^https?:\/\/.+/; + export class ChatDragAndDrop extends Themable { private readonly overlays: Map = new Map(); @@ -154,7 +151,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; } @@ -172,8 +169,10 @@ 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)) { + // This is an estimation based on the datatransfer types/items + if (containsDragType(e, CodeDataTransfers.NOTEBOOK_CELL_OUTPUT)) { + return ChatDragAndDropType.NOTEBOOK_CELL_OUTPUT; + } else if (containsImageDragType(e)) { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined; } else if (containsDragType(e, 'text/html')) { return ChatDragAndDropType.HTML; @@ -183,9 +182,9 @@ export class ChatDragAndDrop extends Themable { return ChatDragAndDropType.MARKER; } else if (containsDragType(e, DataTransfers.FILES)) { return ChatDragAndDropType.FILE_EXTERNAL; - } else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) { + } else if (containsDragType(e, CodeDataTransfers.EDITORS)) { return ChatDragAndDropType.FILE_INTERNAL; - } else if (containsDragType(e, Mimes.uriList, CodeDataTransfers.FILES, DataTransfers.RESOURCES)) { + } else if (containsDragType(e, Mimes.uriList, CodeDataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.INTERNAL_URI_LIST)) { return ChatDragAndDropType.FOLDER; } @@ -207,124 +206,54 @@ export class ChatDragAndDrop extends Themable { case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol'); case ChatDragAndDropType.MARKER: return localize('problem', 'Problem'); case ChatDragAndDropType.HTML: return localize('url', 'URL'); + case ChatDragAndDropType.NOTEBOOK_CELL_OUTPUT: return localize('notebookOutput', 'Output'); } } - private isImageDnd(e: DragEvent): boolean { - // Image detection should not have false positives, only false negatives are allowed - if (containsDragType(e, 'image')) { - return true; + private async resolveAttachmentsFromDragEvent(e: DragEvent): Promise { + if (!this.isDragEventSupported(e)) { + return []; } - if (containsDragType(e, DataTransfers.FILES)) { - const files = e.dataTransfer?.files; - if (files && files.length > 0) { - const file = files[0]; - return file.type.startsWith('image/'); + if (containsDragType(e, CodeDataTransfers.NOTEBOOK_CELL_OUTPUT)) { + const notebookOutputData = extractNotebookCellOutputDropData(e); + if (notebookOutputData) { + return resolveNotebookOutputAttachContext(notebookOutputData, this.editorService); } - - 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 { - 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); } const editorDragData = extractEditorsDropData(e); - if (editorDragData.length === 0 && !containsDragType(e, DataTransfers.INTERNAL_URI_LIST) && containsDragType(e, Mimes.uriList) && ((containsDragType(e, Mimes.html) || containsDragType(e, Mimes.text)))) { - return this.resolveHTMLAttachContext(e); - } - - return coalesce(await Promise.all(editorDragData.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); + 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); + }))); } - // 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); + const internal = e.dataTransfer?.getData(DataTransfers.INTERNAL_URI_LIST); + if (internal) { + const uriList = UriList.parse(internal); + if (uriList.length) { + return coalesce(await Promise.all( + uriList.map(uri => resolveEditorAttachContext({ resource: URI.parse(uri) }, this.fileService, this.editorService, this.textModelService, this.extensionService, this.dialogService)) + )); } } - return undefined; - } + 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); + } - 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 +277,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; + + while (existingAttachmentNames.has(uniqueName)) { + uniqueName = `${baseName} ${++baseNameInstance}`; + } - for (let appendValue = 2; this.attachmentModel.attachments.some(attachment => attachment.name === finalDisplayName); appendValue++) { - finalDisplayName = `${displayName} ${appendValue}`; - } + existingAttachmentNames.add(uniqueName); + return uniqueName; + }; - const dataFromFile = await this.extractImageFromFile(e); - if (dataFromFile) { - return [await this.createImageVariable(await resizeImage(dataFromFile), finalDisplayName)]; - } + const getImageTransferDataFromUrl = async (url: string): Promise => { + const resource = URI.parse(url); - 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)); - } - } + if (IMAGE_DATA_REGEX.test(url)) { + return { data: convertStringToUInt8Array(url), name: createDisplayName(), resource }; } - } - return variableEntries; - } + if (URL_REGEX.test(url)) { + const data = await this.downloadImageAsUint8Array(url); + if (data) { + return { data, name: createDisplayName(), resource, id: url }; + } + } - 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 +377,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/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index fa505f7d3cb..bc7590c5bce 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -23,12 +23,11 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IListService } from '../../../../../platform/list/browser/listService.js'; -import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; +import { IEditorPane } from '../../../../common/editor.js'; 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'; @@ -60,17 +59,11 @@ export function getEditingSessionContext(accessor: ServicesAccessor, args: any[] const arg0 = args.at(0); const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined; - const chatService = accessor.get(IChatService); const chatWidgetService = accessor.get(IChatWidgetService); const chatEditingService = accessor.get(IChatEditingService); let chatWidget = context ? chatWidgetService.getWidgetBySessionId(context.sessionId) : undefined; if (!chatWidget) { - if (chatService.unifiedViewEnabled) { - // TODO ugly - chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel).find(w => w.isUnifiedPanelWidget); - } else { - chatWidget = chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditingSession).at(0); - } + chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel).find(w => w.supportsChangingModes); } if (!chatWidget?.viewModel) { @@ -127,7 +120,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 +139,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 +161,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' }], @@ -177,16 +170,21 @@ registerAction2(class OpenFileInDiffAction extends WorkingSetAction { async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise { const editorService = accessor.get(IEditorService); + + for (const uri of uris) { - const editedFile = currentEditingSession.getEntry(uri); - if (editedFile?.state.get() === WorkingSetEntryState.Modified) { - await editorService.openEditor({ - original: { resource: URI.from(editedFile.originalURI, true) }, - modified: { resource: URI.from(editedFile.modifiedURI, true) }, - }); - } else { - await editorService.openEditor({ resource: uri }); + + let pane: IEditorPane | undefined = editorService.activeEditorPane; + if (!pane) { + pane = await editorService.openEditor({ resource: uri }); } + + if (!pane) { + return; + } + + const editedFile = currentEditingSession.getEntry(uri); + editedFile?.getEditorIntegration(pane).toggleDiff(undefined, true); } } }); @@ -205,7 +203,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 +229,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' }], @@ -330,6 +328,7 @@ export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor return true; } +// TODO@roblourens this may be obsolete? export class ChatEditingRemoveAllFilesAction extends EditingSessionAction { static readonly ID = 'chatEditing.clearWorkingSet'; @@ -354,7 +353,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 : []; @@ -393,48 +392,6 @@ export class ChatEditingShowChangesAction extends EditingSessionAction { } registerAction2(ChatEditingShowChangesAction); -registerAction2(class AddFilesToWorkingSetAction extends EditingSessionAction { - constructor() { - super({ - id: 'workbench.action.chat.addSelectedFilesToWorkingSet', - title: localize2('workbench.action.chat.addSelectedFilesToWorkingSet.label', "Add Selected Files to Working Set"), - icon: Codicon.attach, - precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), - f1: true - }); - } - - override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise { - const listService = accessor.get(IListService); - const editorGroupService = accessor.get(IEditorGroupsService); - - const uris: URI[] = []; - - for (const group of editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { - for (const selection of group.selectedEditors) { - if (selection.resource) { - uris.push(selection.resource); - } - } - } - - if (uris.length === 0) { - const selection = listService.lastFocusedList?.getSelection(); - if (selection?.length) { - for (const file of selection) { - if (!!file && typeof file === 'object' && 'resource' in file && URI.isUri(file.resource)) { - uris.push(file.resource); - } - } - } - } - - for (const file of uris) { - await chatWidget.attachmentModel.addFile(file); - } - } -}); - registerAction2(class RemoveAction extends Action2 { constructor() { super({ @@ -687,3 +644,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/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index 3b7237f1769..b91ea9bde5e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/code/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'; @@ -70,6 +70,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito private readonly _entry: IModifiedFileEntry, private readonly _editor: ICodeEditor, documentDiffInfo: IObservable, + renderDiffImmediately: boolean, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IAccessibilitySignalService private readonly _accessibilitySignalsService: IAccessibilitySignalService, @@ -147,20 +148,15 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito } // done: render diff - if (!_entry.isCurrentlyBeingModifiedBy.read(r)) { + if (!_entry.isCurrentlyBeingModifiedBy.read(r) || renderDiffImmediately) { + const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); - // Add diff decoration to the UI (unless in diff editor) - if (!this._editor.getOption(EditorOption.inDiffEditor)) { - codeEditorObs.getOption(EditorOption.fontInfo).read(r); - codeEditorObs.getOption(EditorOption.lineHeight).read(r); + codeEditorObs.getOption(EditorOption.fontInfo).read(r); + codeEditorObs.getOption(EditorOption.lineHeight).read(r); - const reviewMode = _entry.reviewMode.read(r); - const diff = documentDiffInfo.read(r); - this._updateDiffRendering(diff, reviewMode); - - } else { - this._clearDiffRendering(); - } + const reviewMode = _entry.reviewMode.read(r); + const diff = documentDiffInfo.read(r); + this._updateDiffRendering(diff, reviewMode, isDiffEditor); } })); @@ -274,7 +270,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito this._diffVisualDecorations.clear(); } - private _updateDiffRendering(diff: IDocumentDiff2, reviewMode: boolean): void { + private _updateDiffRendering(diff: IDocumentDiff2, reviewMode: boolean, diffMode: boolean): void { const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({ ...diffAddDecoration, @@ -370,11 +366,12 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito }); } - if (reviewMode) { + let extraLines = 0; + if (reviewMode && !diffMode) { const domNode = document.createElement('div'); domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; const result = renderLines(source, renderOptions, decorations, domNode); - + extraLines = result.heightInLines; if (!isCreatedContent) { const viewZoneData: IViewZone = { @@ -386,12 +383,14 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); } + } + if (reviewMode || diffMode) { // Add content widget for each diff change const widget = this._editor.invokeWithinContext(accessor => { const instaService = accessor.get(IInstantiationService); - return instaService.createInstance(DiffHunkWidget, diff, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); + return instaService.createInstance(DiffHunkWidget, diff, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : extraLines); }); widget.layout(diffEntry.modified.startLineNumber); @@ -407,7 +406,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito } } - this._diffVisualDecorations.set(modifiedVisualDecorations); + this._diffVisualDecorations.set(!diffMode ? modifiedVisualDecorations : []); }); const diffHunkDecoCollection = this._editor.createDecorationsCollection(diffHunkDecorations); @@ -486,7 +485,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito // ---- navigation logic - reveal(firstOrLast: boolean): void { + reveal(firstOrLast: boolean, preserveFocus?: boolean): void { const decorations = this._diffLineDecorations .getRanges() @@ -497,7 +496,9 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito if (range) { this._editor.setPosition(range.getStartPosition()); this._editor.revealRange(range); - this._editor.focus(); + if (!preserveFocus) { + this._editor.focus(); + } this._currentIndex.set(index, undefined); } } @@ -581,23 +582,23 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito return closestWidget; } - rejectNearestChange(closestWidget: IModifiedFileEntryChangeHunk | undefined): void { + async rejectNearestChange(closestWidget?: IModifiedFileEntryChangeHunk): Promise { closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { - closestWidget.reject(); + await closestWidget.reject(); this.next(true); } } - acceptNearestChange(closestWidget: IModifiedFileEntryChangeHunk | undefined): void { + async acceptNearestChange(closestWidget?: IModifiedFileEntryChangeHunk): Promise { closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { - closestWidget.accept(); + await closestWidget.accept(); this.next(true); } } - async toggleDiff(widget: IModifiedFileEntryChangeHunk | undefined): Promise { + async toggleDiff(widget: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise { if (!this._editor.hasModel()) { return; } @@ -613,38 +614,25 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor); - if (isDiffEditor) { - // normal EDITOR - await this._editorService.openEditor({ - resource: this._entry.modifiedURI, - options: { - selection, - selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport - } - }); - - } else { - // DIFF editor - const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; + // Use the 'show' argument to control the diff state if provided + if (show !== undefined ? show : !isDiffEditor) { + // Open DIFF editor + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName; const diffEditor = await this._editorService.openEditor({ - original: { resource: this._entry.originalURI, options: { selection: undefined } }, - modified: { resource: this._entry.modifiedURI, options: { selection } }, + original: { resource: this._entry.originalURI }, + modified: { resource: this._entry.modifiedURI }, + options: { selection }, label: defaultAgentName ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) }); if (diffEditor && diffEditor.input) { - - // this is needed, passing the selection doesn't seem to work diffEditor.getControl()?.setSelection(selection); - - // 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[] = []; for (const candidate of this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { if (isDiffEditorInput(candidate.editor) @@ -659,6 +647,15 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito } }); } + } else { + // Open normal editor + await this._editorService.openEditor({ + resource: this._entry.modifiedURI, + options: { + selection, + selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport + } + }); } } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 29299765e87..991e8f05f27 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -9,11 +9,11 @@ import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ctxHasEditorModification, ctxHasRequestInProgress, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; 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'; @@ -22,6 +22,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor, TEXT_DIFF_EDITOR_ID } from '../../../../common/editor.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { NOTEBOOK_CELL_LIST_FOCUSED } from '../../../notebook/common/notebookContextKeys.js'; abstract class ChatEditingEditorAction extends Action2 { @@ -72,7 +73,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { ? localize2('next', 'Go to Next Chat Edit') : localize2('prev', 'Go to Previous Chat Edit'), icon: next ? Codicon.arrowDown : Codicon.arrowUp, - precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ctxHasEditorModification), keybinding: { primary: next ? KeyMod.Alt | KeyCode.F5 @@ -80,7 +81,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( ctxHasEditorModification, - EditorContextKeys.focus + ContextKeyExpr.or(EditorContextKeys.focus, NOTEBOOK_CELL_LIST_FOCUSED) ), }, f1: true, @@ -88,7 +89,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'navigate', order: !next ? 2 : 3, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()) + when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasEditorModification) } }); } @@ -128,7 +129,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; @@ -155,37 +156,40 @@ async function openNextOrPreviousChange(accessor: ServicesAccessor, session: ICh return true; } -abstract class AcceptDiscardAction extends ChatEditingEditorAction { +abstract class KeepOrUndoAction extends ChatEditingEditorAction { - constructor(id: string, readonly accept: boolean) { + constructor(id: string, private _keep: boolean) { super({ id, - title: accept + title: _keep ? localize2('accept', 'Keep Chat Edits') : localize2('discard', 'Undo Chat Edits'), - shortTitle: accept + shortTitle: _keep ? localize2('accept2', 'Keep') : localize2('discard2', 'Undo'), - tooltip: accept + tooltip: _keep ? localize2('accept3', 'Keep Chat Edits in this File') : localize2('discard3', 'Undo Chat Edits in this File'), precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), - icon: accept + icon: _keep ? Codicon.check : Codicon.discard, f1: true, keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, - primary: accept + primary: _keep ? KeyMod.CtrlCmd | KeyCode.Enter : KeyMod.CtrlCmd | KeyCode.Backspace }, menu: { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', - order: accept ? 0 : 1, - when: ContextKeyExpr.and(!accept ? ctxReviewModeEnabled : undefined, ctxHasRequestInProgress.negate()) + order: _keep ? 0 : 1, + when: ContextKeyExpr.or( + ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress.negate()), // Inline chat + ContextKeyExpr.and(ctxIsGlobalEditingSession, !_keep ? ctxReviewModeEnabled : undefined), // Panel chat + ) } }); } @@ -194,7 +198,7 @@ abstract class AcceptDiscardAction extends ChatEditingEditorAction { const instaService = accessor.get(IInstantiationService); - if (this.accept) { + if (this._keep) { session.accept(entry.modifiedURI); } else { session.reject(entry.modifiedURI); @@ -204,7 +208,7 @@ abstract class AcceptDiscardAction extends ChatEditingEditorAction { } } -export class AcceptAction extends AcceptDiscardAction { +export class AcceptAction extends KeepOrUndoAction { static readonly ID = 'chatEditor.action.accept'; @@ -213,7 +217,7 @@ export class AcceptAction extends AcceptDiscardAction { } } -export class RejectAction extends AcceptDiscardAction { +export class RejectAction extends KeepOrUndoAction { static readonly ID = 'chatEditor.action.reject'; @@ -233,8 +237,8 @@ abstract class AcceptRejectHunkAction extends ChatEditingEditorAction { icon: _accept ? Codicon.check : Codicon.discard, f1: true, keybinding: { - when: EditorContextKeys.focus, - weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or(EditorContextKeys.focus, NOTEBOOK_CELL_LIST_FOCUSED), + weight: KeybindingWeight.WorkbenchContrib + 1, primary: _accept ? KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter : KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace @@ -247,11 +251,11 @@ abstract class AcceptRejectHunkAction extends ChatEditingEditorAction { ); } - override runChatEditingCommand(_accessor: ServicesAccessor, _session: IChatEditingSession, _entry: IModifiedFileEntry, ctrl: IModifiedFileEntryEditorIntegration, ...args: any[]): Promise | void { + override async runChatEditingCommand(_accessor: ServicesAccessor, _session: IChatEditingSession, _entry: IModifiedFileEntry, ctrl: IModifiedFileEntryEditorIntegration, ...args: any[]): Promise { if (this._accept) { - ctrl.acceptNearestChange(args[0]); + await ctrl.acceptNearestChange(args[0]); } else { - ctrl.rejectNearestChange(args[0]); + await ctrl.rejectNearestChange(args[0]); } } } @@ -260,13 +264,13 @@ class ToggleDiffAction extends ChatEditingEditorAction { constructor() { super({ id: 'chatEditor.action.toggleDiff', - title: localize2('diff', 'Toggle Diff Editor'), + title: localize2('diff', 'Toggle Diff Editor for Chat Edits'), category: CHAT_CATEGORY, toggled: { condition: ContextKeyExpr.or(EditorContextKeys.inDiffEditor, ActiveEditorContext.isEqualTo(TEXT_DIFF_EDITOR_ID))!, icon: Codicon.goToFile, }, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification), icon: Codicon.diffSingle, keybinding: { when: EditorContextKeys.focus, @@ -280,7 +284,7 @@ class ToggleDiffAction extends ChatEditingEditorAction { id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: 2, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()) + when: ContextKeyExpr.and(ctxReviewModeEnabled) }] }); } @@ -294,7 +298,7 @@ class ToggleAccessibleDiffViewAction extends ChatEditingEditorAction { constructor() { super({ id: 'chatEditor.action.showAccessibleDiffView', - title: localize2('accessibleDiff', 'Show Accessible Diff View'), + title: localize2('accessibleDiff', 'Show Accessible Diff View for Chat Edits'), f1: true, precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), keybinding: { @@ -385,7 +389,7 @@ export function registerChatEditorActions() { registerAction2(AcceptAction); registerAction2(RejectAction); registerAction2(class AcceptHunkAction extends AcceptRejectHunkAction { constructor() { super(true); } }); - registerAction2(class AcceptHunkAction extends AcceptRejectHunkAction { constructor() { super(false); } }); + registerAction2(class RejectHunkAction extends AcceptRejectHunkAction { constructor() { super(false); } }); registerAction2(ToggleDiffAction); registerAction2(ToggleAccessibleDiffViewAction); @@ -400,7 +404,7 @@ export function registerChatEditorActions() { }, group: 'navigate', order: -1, - when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasRequestInProgress.negate()), + when: ContextKeyExpr.and(ctxReviewModeEnabled, ctxHasEditorModification), }); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts index d86957b3ca8..361997d3bcc 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts +++ b/code/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")); @@ -109,21 +109,22 @@ class ContextKeyGroup { return; } - const { session, entry, isInlineChat } = tuple; + const { session, entry } = tuple; const chatModel = chatService.getSession(session.chatSessionId); - const isRequestInProgress = chatModel - ? observableFromEvent(this, chatModel.onDidChange, () => chatModel.requestInProgress) + const lastResponse = chatModel + ? observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r) + : undefined; + + const isRequestInProgress = lastResponse + ? observableFromEvent(this, lastResponse.onDidChange, () => !lastResponse.isPendingConfirmation && !lastResponse.isComplete) : constObservable(false); - this._ctxHasEditorModification.set(isInlineChat || entry?.state.read(r) === WorkingSetEntryState.Modified); + this._ctxHasEditorModification.set(entry?.state.read(r) === ModifiedFileEntryState.Modified); this._ctxIsGlobalEditingSession.set(session.isGlobalEditingSession); this._ctxReviewModeEnabled.set(entry ? entry.reviewMode.read(r) : false); - this._ctxHasRequestInProgress.set( - Boolean(entry?.isCurrentlyBeingModifiedBy.read(r)) // any entry changing - || (isInlineChat && isRequestInProgress.read(r)) // inline chat request - ); + this._ctxHasRequestInProgress.set(isRequestInProgress.read(r)); // number of requests const requestCount = chatModel diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index 2f50e60efc7..afcda47869d 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -4,18 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import '../media/chatEditingEditorOverlay.css'; -import { combinedDisposable, DisposableMap, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -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 { combinedDisposable, Disposable, DisposableMap, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, derived, derivedOpts, IObservable, observableFromEvent, observableFromEventOpts, observableSignalFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } 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'; -import { addDisposableGenericMouseMoveListener, append, reset } from '../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; +import { $, addDisposableGenericMouseMoveListener, append } from '../../../../../base/browser/dom.js'; import { assertType } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { AcceptAction, navigationBearingFakeActionId, RejectAction } from './chatEditingEditorActions.js'; @@ -30,39 +27,152 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/edi import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { ObservableEditorSession } from './chatEditingEditorContextKeys.js'; -import { rcut } from '../../../../../base/common/strings.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import * as arrays from '../../../../../base/common/arrays.js'; +import { renderStringAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; -class ChatEditorOverlayWidget { +class ChatEditorOverlayWidget extends Disposable { private readonly _domNode: HTMLElement; - // private readonly _progressNode: HTMLElement; - private readonly _toolbar: WorkbenchToolBar; + private readonly _toolbarNode: HTMLElement; - private readonly _showStore = new DisposableStore(); + private readonly _showStore = this._store.add(new DisposableStore()); private readonly _session = observableValue(this, undefined); private readonly _entry = observableValue(this, undefined); + private readonly _isBusy: IObservable; private readonly _navigationBearings = observableValue<{ changeCount: number; activeIdx: number; entriesCount: number }>(this, { changeCount: -1, activeIdx: -1, entriesCount: -1 }); constructor( private readonly _editor: { focus(): void }, @IChatService private readonly _chatService: IChatService, - @IInstantiationService instaService: IInstantiationService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { + super(); this._domNode = document.createElement('div'); this._domNode.classList.add('chat-editor-overlay-widget'); + this._isBusy = derived(r => { + const session = this._session.read(r); + const chatModel = session && _chatService.getSession(session?.chatSessionId); + + const lastResponse = chatModel + ? observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r) + : undefined; + + return lastResponse + ? observableFromEvent(this, lastResponse.onDidChange, () => !lastResponse.isPendingConfirmation && !lastResponse.isComplete).read(r) + : false; + }); + + const requestMessage = derived(r => { + + const session = this._session.read(r); + const chatModel = this._chatService.getSession(session?.chatSessionId ?? ''); + if (!session || !chatModel) { + return undefined; + } + + const response = this._entry.read(r)?.lastModifyingResponse.read(r); + if (!response) { + return { message: localize('working', "Working...") }; + } + + if (response.isPaused.read(r)) { + return { message: localize('paused', "Paused"), paused: true }; + } + + const lastPart = observableFromEventOpts({ equalsFn: arrays.equals }, response.onDidChange, () => response.response.value) + .read(r) + .filter(part => part.kind === 'progressMessage' || part.kind === 'toolInvocation') + .at(-1); + + if (lastPart?.kind === 'toolInvocation') { + return { message: lastPart.invocationMessage }; + + } else if (lastPart?.kind === 'progressMessage') { + return { message: lastPart.content }; + + } else { + return { message: localize('working', "Working...") }; + } + }); + + const progressNode = document.createElement('div'); progressNode.classList.add('chat-editor-overlay-progress'); append(progressNode, renderIcon(ThemeIcon.modify(Codicon.loading, 'spin'))); + const textProgress = append(progressNode, $('span.progress-message')); this._domNode.appendChild(progressNode); - const toolbarNode = document.createElement('div'); - toolbarNode.classList.add('chat-editor-overlay-toolbar'); - this._domNode.appendChild(toolbarNode); + this._store.add(autorun(r => { + const value = requestMessage.read(r); + const busy = this._isBusy.read(r) && !value?.paused; + + this._domNode.classList.toggle('busy', busy); + + if (!busy || !value || this._session.read(r)?.isGlobalEditingSession) { + textProgress.innerText = ''; + } else if (value) { + textProgress.innerText = renderStringAsPlaintext(value.message); + } + })); + + this._toolbarNode = document.createElement('div'); + this._toolbarNode.classList.add('chat-editor-overlay-toolbar'); + + } + + override dispose() { + this.hide(); + super.dispose(); + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + show(session: IChatEditingSession, entry: IModifiedFileEntry | undefined, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { + + this._showStore.clear(); + + transaction(tx => { + this._session.set(session, tx); + this._entry.set(entry, tx); + }); + + this._showStore.add(autorun(r => { + + const entryIndex = indicies.entryIndex.read(r); + const changeIndex = indicies.changeIndex.read(r); + + const entries = session.entries.read(r); + + let activeIdx = entryIndex !== undefined && changeIndex !== undefined + ? changeIndex + : -1; + + let totalChangesCount = 0; + for (let i = 0; i < entries.length; i++) { + const changesCount = entries[i].changesCount.read(r); + totalChangesCount += changesCount; - this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, { + if (entryIndex !== undefined && i < entryIndex) { + activeIdx += changesCount; + } + } + + this._navigationBearings.set({ changeCount: totalChangesCount, activeIdx, entriesCount: entries.length }, undefined); + })); + + + this._domNode.appendChild(this._toolbarNode); + this._showStore.add(toDisposable(() => this._toolbarNode.remove())); + + this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditingEditorContent, { telemetrySource: 'chatEditor.overlayToolbar', hiddenItemStrategy: HiddenItemStrategy.Ignore, toolbarOptions: { @@ -94,7 +204,8 @@ class ChatEditorOverlayWidget { const n = activeIdx === -1 ? '1' : `${activeIdx + 1}`; this.label.innerText = localize('nOfM', "{0} of {1}", n, changeCount); } else { - this.label.innerText = localize('0Of0', "0 of 0"); + // allow-any-unicode-next-line + this.label.innerText = localize('0Of0', "—"); } this.updateTooltip(); @@ -105,15 +216,21 @@ class ChatEditorOverlayWidget { const { changeCount, entriesCount } = that._navigationBearings.get(); if (changeCount === -1 || entriesCount === -1) { return undefined; - } else if (changeCount === 1 && entriesCount === 1) { - return localize('tooltip_11', "1 change in 1 file"); + } + let result: string | undefined; + if (changeCount === 1 && entriesCount === 1) { + result = localize('tooltip_11', "1 change in 1 file"); } else if (changeCount === 1) { - return localize('tooltip_1n', "1 change in {0} files", entriesCount); + result = localize('tooltip_1n', "1 change in {0} files", entriesCount); } else if (entriesCount === 1) { - return localize('tooltip_n1', "{0} changes in 1 file", changeCount); + result = localize('tooltip_n1', "{0} changes in 1 file", changeCount); } else { - return localize('tooltip_nm', "{0} changes in {1} files", changeCount, entriesCount); + result = localize('tooltip_nm', "{0} changes in {1} files", changeCount, entriesCount); } + if (!that._isBusy.get()) { + return result; + } + return localize('tooltip_busy', "{0} - Working...", result); } }; } @@ -168,138 +285,8 @@ class ChatEditorOverlayWidget { }; } - if (action.id === 'inlineChat2.reveal' || action.id === 'workbench.action.chat.openEditSession') { - return new class extends ActionViewItem { - - private _requestMessage: IObservable<{ message: string; paused?: boolean } | undefined>; - - constructor() { - super(undefined, action, options); - - this._requestMessage = derived(r => { - const session = that._session.read(r); - const chatModel = that._chatService.getSession(session?.chatSessionId ?? ''); - if (!session || !chatModel) { - return undefined; - } - - const response = that._entry.read(r)?.isCurrentlyBeingModifiedBy.read(r); - - if (response) { - - if (response?.isPaused.read(r)) { - return { message: localize('paused', "Edits Paused"), paused: true }; - } - - const entry = that._entry.read(r); - if (entry) { - const progress = entry?.rewriteRatio.read(r); - const message = progress === 0 - ? localize('generating', "Generating edits") - : localize('applyingPercentage', "{0}% Applying edits", Math.round(progress * 100)); - - return { message }; - } - } - - if (session.isGlobalEditingSession) { - return undefined; - } - - const request = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)).read(r); - if (!request || request.response?.isComplete) { - return undefined; - } - return { message: request.message.text }; - }); - } - - override render(container: HTMLElement) { - super.render(container); - - container.classList.add('label-item'); - - this._store.add(autorun(r => { - assertType(this.label); - - const value = this._requestMessage.read(r); - if (!value) { - // normal rendering - this.options.icon = true; - this.options.label = false; - reset(this.label); - this.updateClass(); - this.updateLabel(); - this.updateTooltip(); - - } else { - this.options.icon = false; - this.options.label = true; - this.updateClass(); - this.updateTooltip(); - - const message = rcut(value.message, 47); - reset(this.label, message); - } - - const busy = Boolean(value && !value.paused); - that._domNode.classList.toggle('busy', busy); - this.label.classList.toggle('busy', busy); - - })); - } - - protected override getTooltip(): string | undefined { - return this._requestMessage.get()?.message || super.getTooltip(); - } - }; - } return undefined; } - }); - } - - dispose() { - this.hide(); - this._showStore.dispose(); - this._toolbar.dispose(); - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - show(session: IChatEditingSession, entry: IModifiedFileEntry | undefined, indicies: { entryIndex: IObservable; changeIndex: IObservable }) { - - this._showStore.clear(); - - transaction(tx => { - this._session.set(session, tx); - this._entry.set(entry, tx); - }); - - this._showStore.add(autorun(r => { - - const entryIndex = indicies.entryIndex.read(r); - const changeIndex = indicies.changeIndex.read(r); - - const entries = session.entries.read(r); - - let activeIdx = entryIndex !== undefined && changeIndex !== undefined - ? changeIndex - : -1; - - let totalChangesCount = 0; - for (let i = 0; i < entries.length; i++) { - const changesCount = entries[i].changesCount.read(r); - totalChangesCount += changesCount; - - if (entryIndex !== undefined && i < entryIndex) { - activeIdx += changesCount; - } - } - - this._navigationBearings.set({ changeCount: totalChangesCount, activeIdx, entriesCount: entries.length }, undefined); })); } @@ -391,7 +378,7 @@ class ChatEditingOverlayController { if (!response) { return false; } - return observableFromEvent(this, response.onDidChange, () => !response.isComplete).read(r); + return observableFromEvent(this, response.onDidChange, () => !response.isComplete && !response.isPendingConfirmation).read(r); }); this._store.add(autorun(r => { @@ -405,8 +392,14 @@ class ChatEditingOverlayController { const { session, entry } = data; + if (!session.isGlobalEditingSession && !inlineChatService.hideOnRequest.read(r)) { + // inline chat - no chat overlay unless hideOnRequest is on + hide(); + return; + } + 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/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 48a0038e739..781cc373a67 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -6,20 +6,20 @@ import { assert } from '../../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { IReference, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { observableValue, IObservable, ITransaction, autorun, transaction } from '../../../../../base/common/observable.js'; +import { ITransaction, autorun, observableValue, transaction } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { ISingleEditOperation, EditOperation } from '../../../../../editor/common/core/editOperation.js'; +import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { OverviewRulerLane, MinimapPosition, ITextModel, IModelDeltaDecoration } from '../../../../../editor/common/model.js'; +import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane } from '../../../../../editor/common/model.js'; import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js'; import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js'; import { OffsetEdits } from '../../../../../editor/common/model/textModelOffsetEdit.js'; @@ -27,24 +27,24 @@ import { IEditorWorkerService } from '../../../../../editor/common/services/edit import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; +import { TextModelChangeRecorder } from '../../../../../editor/contrib/inlineCompletions/browser/model/changeRecorder.js'; import { localize } from '../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; -import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; -import { SaveReason, IEditorPane } from '../../../../common/editor.js'; +import { IEditorPane, SaveReason } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { IResolvedTextFileEditorModel, ITextFileService, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; +import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; -import { IModifiedFileEntry, ChatEditKind, WorkingSetEntryState, IModifiedFileEntryEditorIntegration } 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'; import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js'; -import { AbstractChatEditingModifiedFileEntry, pendingRewriteMinimap, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry, pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.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; @@ -91,9 +91,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, []); }, 500)); private _editDecorations: string[] = []; - - private readonly _diffTrimWhitespace: IObservable; - readonly originalURI: URI; constructor( @@ -128,7 +125,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); @@ -159,17 +156,12 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this._clearCurrentEditLineDecoration(); })); - this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); - this._register(autorun(r => { - this._diffTrimWhitespace.read(r); - this._updateDiffInfoSeq(); - })); - const resourceFilter = this._register(new MutableDisposable()); this._register(autorun(r => { - const res = this.isCurrentlyBeingModifiedBy.read(r); - if (res) { - const req = res.session.getRequests().find(value => value.id === res.requestId); + const inProgress = this._lastModifyingResponseInProgressObs.read(r); + if (inProgress) { + const res = this._lastModifyingResponseObs.read(r); + const req = res && res.session.getRequests().find(value => value.id === res.requestId); resourceFilter.value = markerService.installResourceFilter(this.modifiedURI, req?.message.text || localize('default', "Chat Edits")); } else { resourceFilter.clear(); @@ -235,7 +227,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie const e_sum = this._edit; const e_ai = edit; this._edit = e_sum.compose(e_ai); - } else { // e_ai @@ -268,14 +259,15 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } this._allEditsAreFromUs = false; + this._userEditScheduler.schedule(); this._updateDiffInfoSeq(); 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 +311,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); @@ -329,8 +321,15 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this._updateDiffInfoSeq(); this._rewriteRatioObs.set(1, tx); this._editDecorationClear.schedule(); + } }); + if (isLastEdits) { + await this._textFileService.save(this.modifiedModel.uri, { + reason: SaveReason.AUTO, + skipSaveParticipants: true, + }); + } } private async _acceptHunk(change: DetailedLineRangeMapping): Promise { @@ -346,7 +345,8 @@ 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._notifyAction('accepted'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); return true; @@ -364,7 +364,8 @@ 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._notifyAction('rejected'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); return true; @@ -375,9 +376,11 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie this._isEditFromUs = true; try { let result: ISingleEditOperation[] = []; - this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => { - result = undoEdits; - return null; + TextModelChangeRecorder.editWithMetadata({ source: 'Chat.applyEdits' }, () => { + this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => { + result = undoEdits; + return null; + }); }); return result; } finally { @@ -401,15 +404,22 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie return undefined; } + if (this.state.get() !== ModifiedFileEntryState.Modified) { + this._diffInfo.set(nullDocumentDiff, undefined); + return nullDocumentDiff; + } + const docVersionNow = this.modifiedModel.getVersionId(); const snapshotVersionNow = this.originalModel.getVersionId(); - const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); - const diff = await this._editorWorkerService.computeDiff( this.originalModel.uri, this.modifiedModel.uri, - { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, + { + ignoreTrimWhitespace: false, // NEVER ignore whitespace so that undo/accept edits are correct and so that all changes (1 of 2) are spelled out + computeMoves: false, + maxComputationTimeMs: 3000 + }, 'advanced' ); @@ -451,15 +461,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); } @@ -495,6 +507,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } satisfies IDocumentDiff2; }); - return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo); + return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo, false); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index eb68edac8ff..e0b5ebe19ed 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { clamp } from '../../../../../base/common/numbers.js'; -import { autorun, derived, IObservable, ITransaction, observableValue } from '../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, ITransaction, observableFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; @@ -22,7 +23,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,12 +51,19 @@ 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; + protected readonly _lastModifyingResponseObs = observableValueOpts({ equalsFn: (a, b) => a?.requestId === b?.requestId }, undefined); + readonly lastModifyingResponse: IObservable = this._lastModifyingResponseObs; + + protected readonly _lastModifyingResponseInProgressObs = this._lastModifyingResponseObs.map((value, r) => { + return value && observableFromEvent(this, value.onDidChange, () => !value.isComplete && !value.isPendingConfirmation).read(r); + }); + protected readonly _rewriteRatioObs = observableValue(this, 0); readonly rewriteRatio: IObservable = this._rewriteRatioObs; @@ -81,6 +89,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im readonly abstract originalURI: URI; + protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifyAction('userModified'), 1000)); + constructor( readonly modifiedURI: URI, protected _telemetryInfo: IModifiedEntryTelemetryInfo, @@ -119,14 +129,47 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im return tempValue ?? configuredValue === 0; }); + this._store.add(toDisposable(() => this._lastModifyingResponseObs.set(undefined, undefined))); + const autoSaveOff = this._store.add(new MutableDisposable()); this._store.add(autorun(r => { - if (this.isCurrentlyBeingModifiedBy.read(r)) { + if (this._lastModifyingResponseInProgressObs.read(r)) { autoSaveOff.value = _fileConfigService.disableAutoSave(this.modifiedURI); } else { autoSaveOff.clear(); } })); + + this._store.add(autorun(r => { + const inProgress = this._lastModifyingResponseInProgressObs.read(r); + if (inProgress === false && !this.reviewMode.read(r)) { + // AUTO accept mode (when request is done) + + const acceptTimeout = this._autoAcceptTimeout.get() * 1000; + const future = Date.now() + acceptTimeout; + const update = () => { + + const reviewMode = this.reviewMode.get(); + if (reviewMode) { + // switched back to review mode + this._autoAcceptCtrl.set(undefined, undefined); + return; + } + + const remain = Math.round(future - Date.now()); + if (remain <= 0) { + this.accept(undefined); + } else { + const handle = setTimeout(update, 100); + this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => { + clearTimeout(handle); + this._autoAcceptCtrl.set(undefined, undefined); + }), undefined); + } + }; + update(); + } + })); } override dispose(): void { @@ -146,7 +189,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 +204,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,20 +219,20 @@ 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; } + this._notifyAction('rejected'); await this._doReject(tx); - this._stateObs.set(WorkingSetEntryState.Rejected, tx); + this._stateObs.set(ModifiedFileEntryState.Rejected, tx); this._autoAcceptCtrl.set(undefined, tx); - this._notifyAction('rejected'); } protected abstract _doReject(tx: ITransaction | undefined): Promise; - private _notifyAction(outcome: 'accepted' | 'rejected') { + protected _notifyAction(outcome: 'accepted' | 'rejected' | 'userModified') { this._chatService.notifyUserAction({ action: { kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome }, agentId: this._telemetryInfo.agentId, @@ -224,6 +267,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im acceptStreamingEditsStart(responseModel: IChatResponseModel, tx: ITransaction) { this._resetEditsState(tx); this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); + this._lastModifyingResponseObs.set(responseModel, tx); this._autoAcceptCtrl.get()?.cancel(); const undoRedoElement = this._createUndoRedoElement(responseModel); @@ -242,33 +286,6 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im if (await this._areOriginalAndModifiedIdentical()) { // ACCEPT if identical this.accept(tx); - - } else if (!this.reviewMode.get() && !this._autoAcceptCtrl.get()) { - // AUTO accept mode - - const acceptTimeout = this._autoAcceptTimeout.get() * 1000; - const future = Date.now() + acceptTimeout; - const update = () => { - - const reviewMode = this.reviewMode.get(); - if (reviewMode) { - // switched back to review mode - this._autoAcceptCtrl.set(undefined, undefined); - return; - } - - const remain = Math.round(future - Date.now()); - if (remain <= 0) { - this.accept(undefined); - } else { - const handle = setTimeout(update, 100); - this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => { - clearTimeout(handle); - this._autoAcceptCtrl.set(undefined, undefined); - }), undefined); - } - }; - update(); } } @@ -309,6 +326,6 @@ export interface ISnapshotEntry { readonly original: string; readonly current: string; readonly originalToCurrentEdit: OffsetEdit; - readonly state: WorkingSetEntryState; + readonly state: ModifiedFileEntryState; telemetryInfo: IModifiedEntryTelemetryInfo; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index d687d403583..2df60310898 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/code/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,24 +258,28 @@ 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) { - 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(); + this._notifyAction('rejected'); return; } if (!e.rawEvents.length) { return; } + + if (currentState === ModifiedFileEntryState.Rejected) { + return; + } + if (isTransientIPyNbExtensionEvent(this.modifiedModel.notebookType, e)) { return; } this._allEditsAreFromUs = false; + this._userEditScheduler.schedule(); // Changes to cell text is sync'ed and handled separately. // See ChatEditingNotebookCellEntry._mirrorEdits @@ -395,8 +404,8 @@ 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; @@ -478,6 +487,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie // create a snapshot of the current state of the model, before the next set of edits let initial = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit); let last = ''; + let redoState = ModifiedFileEntryState.Rejected; return { type: UndoRedoElementType.Resource, @@ -487,11 +497,32 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie confirmBeforeUndo: false, undo: async () => { last = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit); - restoreSnapshot(this.modifiedModel, initial); + this._isEditFromUs = true; + try { + restoreSnapshot(this.modifiedModel, initial); + restoreSnapshot(this.originalModel, initial); + } finally { + this._isEditFromUs = false; + } + redoState = this._stateObs.get() === ModifiedFileEntryState.Accepted ? ModifiedFileEntryState.Accepted : ModifiedFileEntryState.Rejected; + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); + this.updateCellDiffInfo([], undefined); + this.initializeModelsFromDiff(); + this._notifyAction('userModified'); }, redo: async () => { initial = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit); - restoreSnapshot(this.modifiedModel, last); + this._isEditFromUs = true; + try { + restoreSnapshot(this.modifiedModel, last); + restoreSnapshot(this.originalModel, last); + } finally { + this._isEditFromUs = false; + } + this._stateObs.set(redoState, undefined); + this.updateCellDiffInfo([], undefined); + this.initializeModelsFromDiff(); + this._notifyAction('userModified'); } }; } @@ -575,7 +606,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); @@ -652,8 +683,9 @@ 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); + this._notifyAction(accepted ? 'accepted' : 'rejected'); } } @@ -989,9 +1021,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/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 498740b82a4..029acf012b2 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/code/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'; @@ -220,6 +220,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic // multiple times during the process of response streaming. const editsSeen: ({ seen: number; streaming: IStreamingEdits } | undefined)[] = []; + let editorDidChange = false; + const editorListener = Event.once(this._editorService.onDidActiveEditorChange)(() => { + editorDidChange = true; + }); + const editedFilesExist = new ResourceMap>(); const ensureEditorOpen = (partUri: URI) => { const uri = CellUri.parse(partUri)?.notebook ?? partUri; @@ -233,8 +238,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return; } const activeUri = this._editorService.activeEditorPane?.input.resource; - const inactive = this._editorService.activeEditorPane?.input instanceof ChatEditorInput && this._editorService.activeEditorPane.input.sessionId === session.chatSessionId || - Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); + const inactive = editorDidChange + || this._editorService.activeEditorPane?.input instanceof ChatEditorInput && this._editorService.activeEditorPane.input.sessionId === session.chatSessionId + || Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); this._editorService.openEditor({ resource: uri, options: { inactive, preserveFocus: true, pinned: true } }); })); }; @@ -250,6 +256,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsSeen.length = 0; editedFilesExist.clear(); + editorListener.dispose(); }; const handleResponseParts = async () => { @@ -394,7 +401,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( @@ -420,7 +427,7 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider } const isModified = this._modifiedUris.get().some(e => e.toString() === uri.toString()); if (isModified) { - const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName; return { weight: 1000, letter: Codicon.diffModified, @@ -445,11 +452,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 +469,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 +502,7 @@ class ChatEditingMultiDiffSource implements IResolvedMultiDiffSource { }; constructor( - private readonly _currentSession: IObservable + private readonly _currentSession: IObservable, + private readonly _showPreviousChanges: boolean ) { } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 98941d4cd1d..d07f8f070bc 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js'; +import { findLast } from '../../../../../base/common/arraysFind.js'; import { DeferredPromise, ITask, Sequencer, SequencerByKey, timeout } from '../../../../../base/common/async.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { StringSHA1 } from '../../../../../base/common/hash.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObservable, IReader, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { isEqual, joinPath } from '../../../../../base/common/resources.js'; +import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; -import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; @@ -24,34 +23,28 @@ import { IEditorWorkerService } from '../../../../../editor/common/services/edit import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../../nls.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; -import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; +import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.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, 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'; import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; -import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { ChatEditingSessionStorage, IChatEditingSessionSnapshot, IChatEditingSessionStop, StoredSessionState } from './chatEditingSessionStorage.js'; +import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; import { ChatEditingModifiedNotebookDiff } from './notebook/chatEditingModifiedNotebookDiff.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -const STORAGE_CONTENTS_FOLDER = 'contents'; -const STORAGE_STATE_FILE = 'state.json'; const POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated class ThrottledSequencer extends Sequencer { @@ -121,6 +114,38 @@ function getCurrentAndNextStop(requestId: string, stopId: string | undefined, hi return { current, next }; } +function getFirstAndLastStop(uri: URI, history: readonly IChatEditingSessionSnapshot[]): { current: ResourceMap; next: ResourceMap } | undefined { + let firstStopWithUri: IChatEditingSessionStop | undefined; + for (const snapshot of history) { + const stop = snapshot.stops.find(s => s.entries.has(uri)); + if (stop) { + firstStopWithUri = stop; + break; + } + } + + let lastStopWithUri: ResourceMap | undefined; + for (let i = history.length - 1; i >= 0; i--) { + const snapshot = history[i]; + if (snapshot.postEdit?.has(uri)) { + lastStopWithUri = snapshot.postEdit; + break; + } + + const stop = findLast(snapshot.stops, s => s.entries.has(uri)); + if (stop) { + lastStopWithUri = stop.entries; + break; + } + } + + if (!firstStopWithUri || !lastStopWithUri) { + return undefined; + } + + return { current: firstStopWithUri.entries, next: lastStopWithUri }; +} + export class ChatEditingSession extends Disposable implements IChatEditingSession { private readonly _state = observableValue(this, ChatEditingSessionState.Initial); @@ -138,8 +163,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return this._entriesObs; } - private _workingSet = new ResourceMap(); - private _editorPane: MultiDiffEditor | undefined; get state(): IObservable { @@ -168,12 +191,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 +215,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 { @@ -222,7 +240,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio entries.forEach(entry => { entry.state.read(reader); }); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); })); } @@ -270,8 +287,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. @@ -300,7 +318,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; } @@ -341,13 +360,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); @@ -370,22 +391,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); + } - 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 }); + return observable; } + } + + public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void { + const snapshot = makeEmpty ? this._createEmptySnapshot(undoStop) : this._createSnapshot(requestId, undoStop); const linearHistoryPtr = this._linearHistoryIndex.get(); const newLinearHistory: IChatEditingSessionSnapshot[] = []; @@ -411,8 +440,15 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); } + private _createEmptySnapshot(undoStop: string | undefined): IChatEditingSessionStop { + return { + stopId: undoStop, + entries: new ResourceMap(), + }; + } + 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)); @@ -420,7 +456,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return { stopId: undoStop, - workingSet, entries, }; } @@ -471,8 +506,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 @@ -488,7 +522,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); } @@ -496,7 +530,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; @@ -510,17 +544,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 { @@ -546,7 +575,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 { @@ -565,10 +593,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()) { @@ -579,7 +606,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); @@ -797,7 +824,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._createEmptySnapshot(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(); @@ -826,6 +853,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); } @@ -857,7 +885,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return entry.acceptStreamingEditsEnd(tx); }); - this._onDidChange.fire(ChatEditingSessionChangeType.Other); } /** @@ -896,7 +923,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) { @@ -905,13 +931,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; } @@ -921,8 +945,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); @@ -953,297 +976,3 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } } - -interface StoredSessionState { - readonly initialFileContents: ResourceMap; - readonly pendingSnapshot?: IChatEditingSessionStop; - readonly recentSnapshot: IChatEditingSessionStop; - readonly linearHistoryIndex: number; - readonly linearHistory: readonly IChatEditingSessionSnapshot[]; -} - -class ChatEditingSessionStorage { - constructor( - private readonly chatSessionId: string, - @IFileService private readonly _fileService: IFileService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService, - @ILogService private readonly _logService: ILogService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - ) { } - - private _getStorageLocation(): URI { - const workspaceId = this._workspaceContextService.getWorkspace().id; - return joinPath(this._environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', this.chatSessionId); - } - - public async restoreState(): Promise { - const storageLocation = this._getStorageLocation(); - const fileContents = new Map>(); - const getFileContent = (hash: string) => { - let readPromise = fileContents.get(hash); - if (!readPromise) { - readPromise = this._fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); - fileContents.set(hash, readPromise); - } - 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) { - const entry = await deserializeSnapshotEntry(entryDTO); - entries.set(entry.resource, entry); - } - return entries; - }; - 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 }; - }; - 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 }; - }; - const deserializeChatEditingSessionSnapshot = async (startIndex: number, snapshot: IChatEditingSessionSnapshotDTO2): Promise => { - const stops = await Promise.all(snapshot.stops.map(deserializeChatEditingStopDTO)); - return { startIndex, requestId: snapshot.requestId, stops, postEdit: snapshot.postEdit && await deserializeSnapshotEntriesDTO(snapshot.postEdit) }; - }; - const deserializeSnapshotEntry = async (entry: ISnapshotEntryDTO) => { - return { - resource: URI.parse(entry.resource), - languageId: entry.languageId, - original: await getFileContent(entry.originalHash), - current: await getFileContent(entry.currentHash), - originalToCurrentEdit: OffsetEdit.fromJson(entry.originalToCurrentEdit), - state: entry.state, - snapshotUri: URI.parse(entry.snapshotUri), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: this.chatSessionId, result: undefined } - } satisfies ISnapshotEntry; - }; - try { - const stateFilePath = joinPath(storageLocation, STORAGE_STATE_FILE); - if (! await this._fileService.exists(stateFilePath)) { - this._logService.debug(`chatEditingSession: No editing session state found at ${stateFilePath.toString()}`); - return undefined; - } - this._logService.debug(`chatEditingSession: Restoring editing session at ${stateFilePath.toString()}`); - const stateFileContent = await this._fileService.readFile(stateFilePath); - const data = JSON.parse(stateFileContent.value.toString()) as IChatEditingSessionDTO; - if (!COMPATIBLE_STORAGE_VERSIONS.includes(data.version)) { - return undefined; - } - - let linearHistoryIndex = 0; - const linearHistory = await Promise.all(data.linearHistory.map(snapshot => { - const norm = normalizeSnapshotDtos(snapshot); - const result = deserializeChatEditingSessionSnapshot(linearHistoryIndex, norm); - linearHistoryIndex += norm.stops.length; - return result; - })); - - const initialFileContents = new ResourceMap(); - for (const fileContentDTO of data.initialFileContents) { - initialFileContents.set(URI.parse(fileContentDTO[0]), await getFileContent(fileContentDTO[1])); - } - const pendingSnapshot = data.pendingSnapshot ? await deserializeChatEditingStopDTO(data.pendingSnapshot) : undefined; - const recentSnapshot = await deserializeChatEditingStopDTO(data.recentSnapshot); - - return { - initialFileContents, - pendingSnapshot, - recentSnapshot, - linearHistoryIndex: data.linearHistoryIndex, - linearHistory - }; - } catch (e) { - this._logService.error(`Error restoring chat editing session from ${storageLocation.toString()}`, e); - } - return undefined; - } - - public async storeState(state: StoredSessionState): Promise { - const storageFolder = this._getStorageLocation(); - const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER); - - // prepare the content folder - const existingContents = new Set(); - try { - const stat = await this._fileService.resolve(contentsFolder); - stat.children?.forEach(child => { - if (child.isFile) { - existingContents.add(child.name); - } - }); - } catch (e) { - try { - // does not exist, create - await this._fileService.createFolder(contentsFolder); - } catch (e) { - this._logService.error(`Error creating chat editing session content folder ${contentsFolder.toString()}`, e); - return; - } - } - - const fileContents = new Map(); - const addFileContent = (content: string): string => { - const shaComputer = new StringSHA1(); - shaComputer.update(content); - const sha = shaComputer.digest().substring(0, 7); - fileContents.set(sha, content); - return sha; - }; - const serializeResourceMap = (resourceMap: ResourceMap, serialize: (value: T) => any): ResourceMapDTO => { - return Array.from(resourceMap.entries()).map(([resourceURI, value]) => [resourceURI.toString(), serialize(value)]); - }; - const serializeChatEditingSessionStop = (stop: IChatEditingSessionStop): IChatEditingSessionStopDTO => { - return { - stopId: stop.stopId, - workingSet: serializeResourceMap(stop.workingSet, value => value), - entries: Array.from(stop.entries.values()).map(serializeSnapshotEntry) - }; - }; - const serializeChatEditingSessionSnapshot = (snapshot: IChatEditingSessionSnapshot): IChatEditingSessionSnapshotDTO2 => { - return { - requestId: snapshot.requestId, - stops: snapshot.stops.map(serializeChatEditingSessionStop), - postEdit: snapshot.postEdit ? Array.from(snapshot.postEdit.values()).map(serializeSnapshotEntry) : undefined - }; - }; - const serializeSnapshotEntry = (entry: ISnapshotEntry): ISnapshotEntryDTO => { - return { - resource: entry.resource.toString(), - languageId: entry.languageId, - originalHash: addFileContent(entry.original), - currentHash: addFileContent(entry.current), - originalToCurrentEdit: entry.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)), - state: entry.state, - snapshotUri: entry.snapshotUri.toString(), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } - }; - }; - - try { - const data: IChatEditingSessionDTO = { - version: STORAGE_VERSION, - sessionId: this.chatSessionId, - linearHistory: state.linearHistory.map(serializeChatEditingSessionSnapshot), - linearHistoryIndex: state.linearHistoryIndex, - initialFileContents: serializeResourceMap(state.initialFileContents, value => addFileContent(value)), - pendingSnapshot: state.pendingSnapshot ? serializeChatEditingSessionStop(state.pendingSnapshot) : undefined, - recentSnapshot: serializeChatEditingSessionStop(state.recentSnapshot), - }; - - this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${fileContents.size} files`); - - for (const [hash, content] of fileContents) { - if (!existingContents.has(hash)) { - await this._fileService.writeFile(joinPath(contentsFolder, hash), VSBuffer.fromString(content)); - } - } - - await this._fileService.writeFile(joinPath(storageFolder, STORAGE_STATE_FILE), VSBuffer.fromString(JSON.stringify(data, undefined, 2))); - } catch (e) { - this._logService.debug(`Error storing chat editing session to ${storageFolder.toString()}`, e); - } - } - - public async clearState(): Promise { - const storageFolder = this._getStorageLocation(); - if (await this._fileService.exists(storageFolder)) { - this._logService.debug(`chatEditingSession: Clearing editing session at ${storageFolder.toString()}`); - try { - await this._fileService.del(storageFolder, { recursive: true }); - } catch (e) { - this._logService.debug(`Error clearing chat editing session from ${storageFolder.toString()}`, e); - } - } - } -} - -export interface IChatEditingSessionSnapshot { - /** - * Index of this session in the linear history. It's the sum of the lengths - * of all {@link stops} prior this one. - */ - readonly startIndex: number; - - readonly requestId: string | undefined; - /** - * Edit stops in the request. Always initially populatd with stopId: undefind - * for th request's initial state. - * - * Invariant: never empty. - */ - readonly stops: IChatEditingSessionStop[]; - - /** Stop that represents changes after the last undo stop, kept for diffing purposes. */ - readonly postEdit: ResourceMap | undefined; -} - -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[]; -} - - -interface IChatEditingSessionSnapshotDTO { - readonly requestId: string | undefined; - readonly workingSet: ResourceMapDTO; - readonly entries: ISnapshotEntryDTO[]; -} - -interface IChatEditingSessionSnapshotDTO2 { - readonly requestId: string | undefined; - readonly stops: IChatEditingSessionStopDTO[]; - readonly postEdit: ISnapshotEntryDTO[] | undefined; -} - -interface ISnapshotEntryDTO { - readonly resource: string; - readonly languageId: string; - readonly originalHash: string; - readonly currentHash: string; - readonly originalToCurrentEdit: IOffsetEdit; - readonly state: WorkingSetEntryState; - readonly snapshotUri: string; - readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; -} - -interface IModifiedEntryTelemetryInfoDTO { - readonly requestId: string; - readonly agentId?: string; - readonly command?: string; -} - -type ResourceMapDTO = [string, T][]; - -const COMPATIBLE_STORAGE_VERSIONS = [1, 2]; -const STORAGE_VERSION = 2; - -/** Old history uses IChatEditingSessionSnapshotDTO, new history uses IChatEditingSessionSnapshotDTO. */ -interface IChatEditingSessionDTO { - readonly version: number; - readonly sessionId: string; - readonly recentSnapshot: (IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO); - readonly linearHistory: (IChatEditingSessionSnapshotDTO2 | IChatEditingSessionSnapshotDTO)[]; - readonly linearHistoryIndex: number; - readonly pendingSnapshot: (IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO) | undefined; - readonly initialFileContents: ResourceMapDTO; -} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts new file mode 100644 index 00000000000..7eba4f40c87 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -0,0 +1,304 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { StringSHA1 } from '../../../../../base/common/hash.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { joinPath } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { OffsetEdit, ISingleOffsetEdit, IOffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { WorkingSetDisplayMetadata, ModifiedFileEntryState } from '../../common/chatEditingService.js'; + +const STORAGE_CONTENTS_FOLDER = 'contents'; +const STORAGE_STATE_FILE = 'state.json'; + +export interface StoredSessionState { + readonly initialFileContents: ResourceMap; + readonly pendingSnapshot?: IChatEditingSessionStop; + readonly recentSnapshot: IChatEditingSessionStop; + readonly linearHistoryIndex: number; + readonly linearHistory: readonly IChatEditingSessionSnapshot[]; +} + +export class ChatEditingSessionStorage { + constructor( + private readonly chatSessionId: string, + @IFileService private readonly _fileService: IFileService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ILogService private readonly _logService: ILogService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + ) { } + + protected _getStorageLocation(): URI { + const workspaceId = this._workspaceContextService.getWorkspace().id; + return joinPath(this._environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', this.chatSessionId); + } + + public async restoreState(): Promise { + const storageLocation = this._getStorageLocation(); + const fileContents = new Map>(); + const getFileContent = (hash: string) => { + let readPromise = fileContents.get(hash); + if (!readPromise) { + readPromise = this._fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); + fileContents.set(hash, readPromise); + } + return readPromise; + }; + const deserializeSnapshotEntriesDTO = async (dtoEntries: ISnapshotEntryDTO[]): Promise> => { + const entries = new ResourceMap(); + for (const entryDTO of dtoEntries) { + const entry = await deserializeSnapshotEntry(entryDTO); + entries.set(entry.resource, entry); + } + return entries; + }; + const deserializeChatEditingStopDTO = async (stopDTO: IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO): Promise => { + const entries = await deserializeSnapshotEntriesDTO(stopDTO.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 }], postEdit: undefined }; + }; + const deserializeChatEditingSessionSnapshot = async (startIndex: number, snapshot: IChatEditingSessionSnapshotDTO2): Promise => { + const stops = await Promise.all(snapshot.stops.map(deserializeChatEditingStopDTO)); + return { startIndex, requestId: snapshot.requestId, stops, postEdit: snapshot.postEdit && await deserializeSnapshotEntriesDTO(snapshot.postEdit) }; + }; + const deserializeSnapshotEntry = async (entry: ISnapshotEntryDTO) => { + return { + resource: URI.parse(entry.resource), + languageId: entry.languageId, + original: await getFileContent(entry.originalHash), + current: await getFileContent(entry.currentHash), + originalToCurrentEdit: OffsetEdit.fromJson(entry.originalToCurrentEdit), + state: entry.state, + snapshotUri: URI.parse(entry.snapshotUri), + telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: this.chatSessionId, result: undefined } + } satisfies ISnapshotEntry; + }; + try { + const stateFilePath = joinPath(storageLocation, STORAGE_STATE_FILE); + if (! await this._fileService.exists(stateFilePath)) { + this._logService.debug(`chatEditingSession: No editing session state found at ${stateFilePath.toString()}`); + return undefined; + } + this._logService.debug(`chatEditingSession: Restoring editing session at ${stateFilePath.toString()}`); + const stateFileContent = await this._fileService.readFile(stateFilePath); + const data = JSON.parse(stateFileContent.value.toString()) as IChatEditingSessionDTO; + if (!COMPATIBLE_STORAGE_VERSIONS.includes(data.version)) { + return undefined; + } + + let linearHistoryIndex = 0; + const linearHistory = await Promise.all(data.linearHistory.map(snapshot => { + const norm = normalizeSnapshotDtos(snapshot); + const result = deserializeChatEditingSessionSnapshot(linearHistoryIndex, norm); + linearHistoryIndex += norm.stops.length; + return result; + })); + + const initialFileContents = new ResourceMap(); + for (const fileContentDTO of data.initialFileContents) { + initialFileContents.set(URI.parse(fileContentDTO[0]), await getFileContent(fileContentDTO[1])); + } + const pendingSnapshot = data.pendingSnapshot ? await deserializeChatEditingStopDTO(data.pendingSnapshot) : undefined; + const recentSnapshot = await deserializeChatEditingStopDTO(data.recentSnapshot); + + return { + initialFileContents, + pendingSnapshot, + recentSnapshot, + linearHistoryIndex: data.linearHistoryIndex, + linearHistory + }; + } catch (e) { + this._logService.error(`Error restoring chat editing session from ${storageLocation.toString()}`, e); + } + return undefined; + } + + public async storeState(state: StoredSessionState): Promise { + const storageFolder = this._getStorageLocation(); + const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER); + + // prepare the content folder + const existingContents = new Set(); + try { + const stat = await this._fileService.resolve(contentsFolder); + stat.children?.forEach(child => { + if (child.isFile) { + existingContents.add(child.name); + } + }); + } catch (e) { + try { + // does not exist, create + await this._fileService.createFolder(contentsFolder); + } catch (e) { + this._logService.error(`Error creating chat editing session content folder ${contentsFolder.toString()}`, e); + return; + } + } + + const fileContents = new Map(); + const addFileContent = (content: string): string => { + const shaComputer = new StringSHA1(); + shaComputer.update(content); + const sha = shaComputer.digest().substring(0, 7); + fileContents.set(sha, content); + return sha; + }; + const serializeResourceMap = (resourceMap: ResourceMap, serialize: (value: T) => any): ResourceMapDTO => { + return Array.from(resourceMap.entries()).map(([resourceURI, value]) => [resourceURI.toString(), serialize(value)]); + }; + const serializeChatEditingSessionStop = (stop: IChatEditingSessionStop): IChatEditingSessionStopDTO => { + return { + stopId: stop.stopId, + entries: Array.from(stop.entries.values()).map(serializeSnapshotEntry) + }; + }; + const serializeChatEditingSessionSnapshot = (snapshot: IChatEditingSessionSnapshot): IChatEditingSessionSnapshotDTO2 => { + return { + requestId: snapshot.requestId, + stops: snapshot.stops.map(serializeChatEditingSessionStop), + postEdit: snapshot.postEdit ? Array.from(snapshot.postEdit.values()).map(serializeSnapshotEntry) : undefined + }; + }; + const serializeSnapshotEntry = (entry: ISnapshotEntry): ISnapshotEntryDTO => { + return { + resource: entry.resource.toString(), + languageId: entry.languageId, + originalHash: addFileContent(entry.original), + currentHash: addFileContent(entry.current), + originalToCurrentEdit: entry.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)), + state: entry.state, + snapshotUri: entry.snapshotUri.toString(), + telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } + }; + }; + + try { + const data: IChatEditingSessionDTO = { + version: STORAGE_VERSION, + sessionId: this.chatSessionId, + linearHistory: state.linearHistory.map(serializeChatEditingSessionSnapshot), + linearHistoryIndex: state.linearHistoryIndex, + initialFileContents: serializeResourceMap(state.initialFileContents, value => addFileContent(value)), + pendingSnapshot: state.pendingSnapshot ? serializeChatEditingSessionStop(state.pendingSnapshot) : undefined, + recentSnapshot: serializeChatEditingSessionStop(state.recentSnapshot), + }; + + this._logService.debug(`chatEditingSession: Storing editing session at ${storageFolder.toString()}: ${fileContents.size} files`); + + for (const [hash, content] of fileContents) { + if (!existingContents.has(hash)) { + await this._fileService.writeFile(joinPath(contentsFolder, hash), VSBuffer.fromString(content)); + } + } + + await this._fileService.writeFile(joinPath(storageFolder, STORAGE_STATE_FILE), VSBuffer.fromString(JSON.stringify(data))); + } catch (e) { + this._logService.debug(`Error storing chat editing session to ${storageFolder.toString()}`, e); + } + } + + public async clearState(): Promise { + const storageFolder = this._getStorageLocation(); + if (await this._fileService.exists(storageFolder)) { + this._logService.debug(`chatEditingSession: Clearing editing session at ${storageFolder.toString()}`); + try { + await this._fileService.del(storageFolder, { recursive: true }); + } catch (e) { + this._logService.debug(`Error clearing chat editing session from ${storageFolder.toString()}`, e); + } + } + } +} + +export interface IChatEditingSessionSnapshot { + /** + * Index of this session in the linear history. It's the sum of the lengths + * of all {@link stops} prior this one. + */ + readonly startIndex: number; + + readonly requestId: string | undefined; + /** + * Edit stops in the request. Always initially populatd with stopId: undefind + * for th request's initial state. + * + * Invariant: never empty. + */ + readonly stops: IChatEditingSessionStop[]; + + /** Stop that represents changes after the last undo stop, kept for diffing purposes. */ + readonly postEdit: ResourceMap | undefined; +} + +export interface IChatEditingSessionStop { + /** Edit stop ID, first for a request is always undefined. */ + stopId: string | undefined; + + readonly entries: ResourceMap; +} + +interface IChatEditingSessionStopDTO { + readonly stopId: string | undefined; + readonly entries: ISnapshotEntryDTO[]; +} + + +interface IChatEditingSessionSnapshotDTO { + readonly requestId: string | undefined; + readonly workingSet: ResourceMapDTO; + readonly entries: ISnapshotEntryDTO[]; +} + +interface IChatEditingSessionSnapshotDTO2 { + readonly requestId: string | undefined; + readonly stops: IChatEditingSessionStopDTO[]; + readonly postEdit: ISnapshotEntryDTO[] | undefined; +} + +interface ISnapshotEntryDTO { + readonly resource: string; + readonly languageId: string; + readonly originalHash: string; + readonly currentHash: string; + readonly originalToCurrentEdit: IOffsetEdit; + readonly state: ModifiedFileEntryState; + readonly snapshotUri: string; + readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; +} + +interface IModifiedEntryTelemetryInfoDTO { + readonly requestId: string; + readonly agentId?: string; + readonly command?: string; +} + +type ResourceMapDTO = [string, T][]; + +const COMPATIBLE_STORAGE_VERSIONS = [1, 2]; +const STORAGE_VERSION = 2; + +/** Old history uses IChatEditingSessionSnapshotDTO, new history uses IChatEditingSessionSnapshotDTO. */ +interface IChatEditingSessionDTO { + readonly version: number; + readonly sessionId: string; + readonly recentSnapshot: (IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO); + readonly linearHistory: (IChatEditingSessionSnapshotDTO2 | IChatEditingSessionSnapshotDTO)[]; + readonly linearHistoryIndex: number; + readonly pendingSnapshot: (IChatEditingSessionStopDTO | IChatEditingSessionSnapshotDTO) | undefined; + readonly initialFileContents: ResourceMapDTO; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts index 9418dc7c6bd..1697e227fe6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts @@ -26,7 +26,8 @@ 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 { CellKind } from '../../../../notebook/common/notebookCommon.js'; +import { ModifiedFileEntryState } from '../../../common/chatEditingService.js'; import { IChatResponseModel } from '../../../common/chatModel.js'; import { pendingRewriteMinimap } from '../chatEditingModifiedFileEntry.js'; @@ -83,8 +84,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 +170,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 +213,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); @@ -229,6 +230,21 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { this._editDecorationClear.schedule(); } + revertMarkdownPreviewState(): void { + if (this.cell.cellKind !== CellKind.Markup) { + return; + } + + const notebookEditor = this.notebookEditorService.retrieveExistingWidgetFromURI(this.notebookUri)?.value; + if (notebookEditor) { + const vm = notebookEditor.getCellByHandle(this.cell.handle); + if (vm?.getEditState() === CellEditState.Editing && + (vm.editStateSource === 'chatEdit' || vm.editStateSource === 'chatEditNavigation')) { + vm?.updateEditState(CellEditState.Preview, 'chatEdit'); + } + } + } + protected _resetEditsState(tx: ITransaction): void { this._isCurrentlyBeingModifiedByObs.set(undefined, tx); this._maxModifiedLineNumber.set(0, tx); @@ -241,7 +257,7 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { private async _acceptHunk(change: DetailedLineRangeMapping): Promise { this._isEditFromUs = true; try { - if (!this._diffInfo.get().changes.includes(change)) { + if (!this._diffInfo.get().changes.filter(c => c.modified.equals(change.modified) && c.original.equals(change.original)).length) { // diffInfo should have model version ids and check them (instead of the caller doing that) return false; } @@ -257,7 +273,8 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { } await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + this.revertMarkdownPreviewState(); + this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); } return true; } @@ -283,7 +300,8 @@ export class ChatEditingNotebookCellEntry extends ObservableDisposable { } await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + this.revertMarkdownPreviewState(); + this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); } return true; } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index ced1613cf14..ff79d9d3ea4 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -4,24 +4,25 @@ *--------------------------------------------------------------------------------------------*/ 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'; import { Range } from '../../../../../../editor/common/core/range.js'; import { nullDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; +import { PrefixSumComputer } from '../../../../../../editor/common/model/prefixSumComputer.js'; import { localize } from '../../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IEditorPane, IResourceDiffEditorInput } from '../../../../../common/editor.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { NotebookDeletedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.js'; import { NotebookInsertedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.js'; import { NotebookModifiedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.js'; import { INotebookTextDiffEditor } from '../../../../notebook/browser/diff/notebookDiffEditorBrowser.js'; -import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor } from '../../../../notebook/browser/notebookBrowser.js'; +import { CellEditState, getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor } from '../../../../notebook/browser/notebookBrowser.js'; import { INotebookEditorService } from '../../../../notebook/browser/services/notebookEditorService.js'; import { NotebookCellTextModel } from '../../../../notebook/common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; @@ -32,6 +33,7 @@ import { ChatAgentLocation } from '../../../common/constants.js'; import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from '../chatEditingCodeEditorIntegration.js'; import { ChatEditingModifiedNotebookEntry } from '../chatEditingModifiedNotebookEntry.js'; import { countChanges, ICellDiffInfo, sortCellChanges } from './notebookCellChanges.js'; +import { OverlayToolbarDecorator } from './overlayToolbarDecorator.js'; export class ChatEditingNotebookEditorIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { private integration: ChatEditingNotebookEditorWidgetIntegration; @@ -74,14 +76,14 @@ export class ChatEditingNotebookEditorIntegration extends Disposable implements enableAccessibleDiffView(): void { this.integration.enableAccessibleDiffView(); } - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { - this.integration.acceptNearestChange(change); + acceptNearestChange(change: IModifiedFileEntryChangeHunk | undefined): Promise { + return this.integration.acceptNearestChange(change); } - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { - this.integration.rejectNearestChange(change); + rejectNearestChange(change: IModifiedFileEntryChangeHunk | undefined): Promise { + return this.integration.rejectNearestChange(change); } - toggleDiff(change: IModifiedFileEntryChangeHunk | undefined): Promise { - return this.integration.toggleDiff(change); + toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise { + return this.integration.toggleDiff(change, show); } public override dispose(): void { @@ -94,19 +96,20 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I private readonly _currentIndex = observableValue(this, -1); readonly currentIndex: IObservable = this._currentIndex; - private readonly _currentChange = observableValue<{ change: ICellDiffInfo; index: number } | undefined>(this, undefined); - readonly currentChange: IObservable<{ change: ICellDiffInfo; index: number } | undefined> = this._currentChange; - private deletedCellDecorator: NotebookDeletedCellDecorator | undefined; private insertedCellDecorator: NotebookInsertedCellDecorator | undefined; private modifiedCellDecorator: NotebookModifiedCellDecorator | undefined; + private overlayToolbarDecorator: OverlayToolbarDecorator | undefined; private readonly cellEditorIntegrations = new Map }>(); - private readonly mdCellEditorAttached = observableValue(this, -1); + private readonly markdownEditState = observableValue(this, ''); private markupCellListeners = new Map(); + private sortedCellChanges: ICellDiffInfo[] = []; + private changeIndexComputer: PrefixSumComputer = new PrefixSumComputer(new Uint32Array(0)); + constructor( private readonly _entry: ChatEditingModifiedNotebookEntry, private readonly notebookEditor: INotebookEditor, @@ -118,6 +121,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I @IChatAgentService private readonly _chatAgentService: IChatAgentService, @INotebookEditorService notebookEditorService: INotebookEditorService, @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @ILogService private readonly logService: ILogService, ) { super(); @@ -174,6 +178,21 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } })); + this._register(autorun(r => { + this.sortedCellChanges = sortCellChanges(cellChanges.read(r)); + const indexes: number[] = []; + for (const change of this.sortedCellChanges) { + indexes.push(change.type === 'insert' || change.type === 'delete' ? 1 + : change.type === 'modified' ? change.diff.read(r).changes.length + : 0); + } + + this.changeIndexComputer = new PrefixSumComputer(new Uint32Array(indexes)); + if (this.changeIndexComputer.getTotalSum() === 0) { + this.revertMarkupCellState(); + } + })); + // Build cell integrations (responsible for navigating changes within a cell and decorating cell text changes) this._register(autorun(r => { if (this.notebookEditor.textModel !== this.notebookModel) { @@ -189,7 +208,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I }); return; } - this.mdCellEditorAttached.read(r); + this.markdownEditState.read(r); const validCells = new Set(); changes.forEach((change) => { @@ -203,20 +222,18 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I if (!cell || !originalModel || !modifiedModel) { return; } - if (!editor) { - if (!this.markupCellListeners.has(cell.handle) && cell.cellKind === CellKind.Markup) { - const cellModel = this.notebookEditor.getViewModel()?.viewCells.find(c => c.handle === cell.handle); - if (cellModel) { - const listener = cellModel.onDidChangeEditorAttachState(() => { - if (cellModel.editorAttached) { - this.mdCellEditorAttached.set(cell.handle, undefined); - listener.dispose(); - this.markupCellListeners.delete(cell.handle); - } - }); - this.markupCellListeners.set(cell.handle, listener); - } + if (cell.cellKind === CellKind.Markup && !this.markupCellListeners.has(cell.handle)) { + const cellModel = this.notebookEditor.getViewModel()?.viewCells.find(c => c.handle === cell.handle); + if (cellModel) { + const listener = cellModel.onDidChangeState((e) => { + if (e.editStateChanged) { + setTimeout(() => this.markdownEditState.set(cellModel.handle + '-' + cellModel.getEditState(), undefined), 0); + } + }); + this.markupCellListeners.set(cell.handle, listener); } + } + if (!editor) { return; } const diff = { @@ -235,7 +252,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } } else { const diff2 = observableValue(`diff${cell.handle}`, diff); - const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2); + const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2, true); this.cellEditorIntegrations.set(cell, { integration, diff: diff2 }); this._register(integration); this._register(editor.onDidDispose(() => { @@ -260,32 +277,6 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I }); })); - this._register(autorun(r => { - const currentChange = this.currentChange.read(r); - if (!currentChange) { - this._currentIndex.set(-1, undefined); - return; - } - - let index = 0; - const sortedCellChanges = sortCellChanges(cellChanges.read(r)); - for (const change of sortedCellChanges) { - if (currentChange && currentChange.change === change) { - if (change.type === 'modified') { - index += currentChange.index; - } - break; - } - if (change.type === 'insert' || change.type === 'delete') { - index++; - } else if (change.type === 'modified') { - index += change.diff.read(r).changes.length; - } - } - - this._currentIndex.set(index, undefined); - })); - const cellsAreVisible = onDidChangeVisibleRanges.map(v => v.length > 0); const debouncedChanges = debouncedObservable(cellChanges, 10); this._register(autorun(r => { @@ -297,18 +288,43 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const modifiedChanges = changes.filter(c => c.type === 'modified'); this.createDecorators(); - this.insertedCellDecorator?.apply(changes); - this.modifiedCellDecorator?.apply(modifiedChanges); - this.deletedCellDecorator?.apply(changes, originalModel); + // If all cells are just inserts, then no need to show any decorations. + if (changes.every(c => c.type === 'insert')) { + this.insertedCellDecorator?.apply([]); + this.modifiedCellDecorator?.apply([]); + this.deletedCellDecorator?.apply([], originalModel); + this.overlayToolbarDecorator?.decorate([]); + } else { + this.insertedCellDecorator?.apply(changes); + this.modifiedCellDecorator?.apply(modifiedChanges); + this.deletedCellDecorator?.apply(changes, originalModel); + this.overlayToolbarDecorator?.decorate(changes.filter(c => c.type === 'insert' || c.type === 'modified')); + } })); } + private getCurrentChange() { + const currentIndex = Math.min(this._currentIndex.get(), this.changeIndexComputer.getTotalSum() - 1); + const index = this.changeIndexComputer.getIndexOf(currentIndex); + const change = this.sortedCellChanges[index.index]; + + return change ? { change, index: index.remainder } : undefined; + } + + private updateCurrentIndex(change: ICellDiffInfo, indexInCell: number = 0) { + const index = this.sortedCellChanges.indexOf(change); + const changeIndex = this.changeIndexComputer.getPrefixSum(index - 1); + const currentIndex = Math.min(changeIndex + indexInCell, this.changeIndexComputer.getTotalSum() - 1); + this._currentIndex.set(currentIndex, undefined); + } + private createDecorators() { const cellChanges = this.cellChanges.get(); const accessibilitySignalService = this.accessibilitySignalService; this.insertedCellDecorator ??= this._register(this.instantiationService.createInstance(NotebookInsertedCellDecorator, this.notebookEditor)); this.modifiedCellDecorator ??= this._register(this.instantiationService.createInstance(NotebookModifiedCellDecorator, this.notebookEditor)); + this.overlayToolbarDecorator ??= this._register(this.instantiationService.createInstance(OverlayToolbarDecorator, this.notebookEditor, this.notebookModel)); if (this.deletedCellDecorator) { this._store.delete(this.deletedCellDecorator); @@ -348,9 +364,9 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } reveal(firstOrLast: boolean): void { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); + const changes = this.sortedCellChanges.filter(c => c.type !== 'unchanged'); if (!changes.length) { - return undefined; + return; } const change = firstOrLast ? changes[0] : changes[changes.length - 1]; this._revealFirstOrLast(change, firstOrLast); @@ -361,20 +377,15 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I case 'insert': case 'modified': { + this.blur(this.getCurrentChange()?.change); const index = firstOrLast || change.type === 'insert' ? 0 : change.diff.get().changes.length - 1; - const cellIntegration = this.getCell(change.modifiedCellIndex); - if (cellIntegration) { - cellIntegration.reveal(firstOrLast); - this._currentChange.set({ change: change, index }, undefined); - return true; - } else { - return this._revealChange(change, index); - } + return this._revealChange(change, index); } case 'delete': + this.blur(this.getCurrentChange()?.change); // reveal the deleted cell decorator this.deletedCellDecorator?.reveal(change.originalCellIndex); - this._currentChange.set({ change: change, index: 0 }, undefined); + this.updateCurrentIndex(change); return true; default: break; @@ -391,16 +402,17 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const textChange = change.diff.get().changes[indexInCell]; const cellViewModel = this.getCellViewModel(change); if (cellViewModel) { - this.revealChangeInView(cellViewModel, textChange?.modified); - this._currentChange.set({ change: change, index: indexInCell }, undefined); + this.updateCurrentIndex(change, indexInCell); + this.revealChangeInView(cellViewModel, textChange?.modified, change) + .catch(err => { this.logService.warn(`Error revealing change in view: ${err}`); }); + return true; } - - return true; + break; } case 'delete': + this.updateCurrentIndex(change); // reveal the deleted cell decorator this.deletedCellDecorator?.reveal(change.originalCellIndex); - this._currentChange.set({ change: change, index: 0 }, undefined); return true; default: break; @@ -410,7 +422,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } private getCellViewModel(change: ICellDiffInfo) { - if (change.type === 'delete' || change.modifiedCellIndex === undefined) { + if (change.type === 'delete' || change.modifiedCellIndex === undefined || change.modifiedCellIndex >= this.notebookModel.cells.length) { return undefined; } const cell = this.notebookModel.cells[change.modifiedCellIndex]; @@ -418,15 +430,40 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I return cellViewModel; } - private async revealChangeInView(cell: ICellViewModel, lines: LineRange | undefined): Promise { + private async revealChangeInView(cell: ICellViewModel, lines: LineRange | undefined, change: ICellDiffInfo): Promise { const targetLines = lines ?? new LineRange(0, 0); - await this.notebookEditor.focusNotebookCell(cell, 'container', { focusEditorLine: targetLines.startLineNumber }); + if (change.type === 'modified' && cell.cellKind === CellKind.Markup && cell.getEditState() === CellEditState.Preview) { + cell.updateEditState(CellEditState.Editing, 'chatEditNavigation'); + } + + const focusTarget = cell.cellKind === CellKind.Code || change.type === 'modified' ? 'editor' : 'container'; + await this.notebookEditor.focusNotebookCell(cell, focusTarget, { focusEditorLine: targetLines.startLineNumber }); await this.notebookEditor.revealRangeInCenterAsync(cell, new Range(targetLines.startLineNumber, 0, targetLines.endLineNumberExclusive, 0)); } + private revertMarkupCellState() { + for (const change of this.sortedCellChanges) { + const cellViewModel = this.getCellViewModel(change); + if (cellViewModel?.cellKind === CellKind.Markup && cellViewModel.getEditState() === CellEditState.Editing && + (cellViewModel.editStateSource === 'chatEditNavigation' || cellViewModel.editStateSource === 'chatEdit')) { + cellViewModel.updateEditState(CellEditState.Preview, 'chatEdit'); + } + } + } + + private blur(change: ICellDiffInfo | undefined) { + if (!change) { + return; + } + const cellViewModel = this.getCellViewModel(change); + if (cellViewModel?.cellKind === CellKind.Markup && cellViewModel.getEditState() === CellEditState.Editing && cellViewModel.editStateSource === 'chatEditNavigation') { + cellViewModel.updateEditState(CellEditState.Preview, 'chatEditNavigation'); + } + } + next(wrap: boolean): boolean { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - const currentChange = this.currentChange.get(); + const changes = this.sortedCellChanges.filter(c => c.type !== 'unchanged'); + const currentChange = this.getCurrentChange(); if (!currentChange) { const firstChange = changes[0]; @@ -445,27 +482,34 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const cellIntegration = this.getCell(currentChange.change.modifiedCellIndex); if (cellIntegration) { if (cellIntegration.next(false)) { - this._currentChange.set({ change: currentChange.change, index: cellIntegration.currentIndex.get() }, undefined); + this.updateCurrentIndex(currentChange.change, cellIntegration.currentIndex.get()); return true; } } - const isLastChangeInCell = currentChange.index === lastChangeIndex(currentChange.change); + const isLastChangeInCell = currentChange.index >= lastChangeIndex(currentChange.change); const index = isLastChangeInCell ? 0 : currentChange.index + 1; const change = isLastChangeInCell ? changes[changes.indexOf(currentChange.change) + 1] : currentChange.change; if (change) { - return this._revealChange(change, index); + if (isLastChangeInCell) { + this.blur(currentChange.change); + } + + if (this._revealChange(change, index)) { + return true; + } } } break; case 'insert': case 'delete': { + this.blur(currentChange.change); // go to next change directly const nextChange = changes[changes.indexOf(currentChange.change) + 1]; - if (nextChange) { - return this._revealFirstOrLast(nextChange, true); + if (nextChange && this._revealFirstOrLast(nextChange, true)) { + return true; } } break; @@ -474,15 +518,18 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } if (wrap) { - return this.next(false); + const firstChange = changes[0]; + if (firstChange) { + return this._revealFirstOrLast(firstChange, true); + } } return false; } previous(wrap: boolean): boolean { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - const currentChange = this.currentChange.get(); + const changes = this.sortedCellChanges.filter(c => c.type !== 'unchanged'); + const currentChange = this.getCurrentChange(); if (!currentChange) { const lastChange = changes[changes.length - 1]; if (lastChange) { @@ -500,27 +547,33 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const cellIntegration = this.getCell(currentChange.change.modifiedCellIndex); if (cellIntegration) { if (cellIntegration.previous(false)) { - this._currentChange.set({ change: currentChange.change, index: cellIntegration.currentIndex.get() }, undefined); + this.updateCurrentIndex(currentChange.change, cellIntegration.currentIndex.get()); return true; } } - const isFirstChangeInCell = currentChange.index === 0; + const isFirstChangeInCell = currentChange.index <= 0; const change = isFirstChangeInCell ? changes[changes.indexOf(currentChange.change) - 1] : currentChange.change; if (change) { const index = isFirstChangeInCell ? lastChangeIndex(change) : currentChange.index - 1; - return this._revealChange(change, index); + if (isFirstChangeInCell) { + this.blur(currentChange.change); + } + if (this._revealChange(change, index)) { + return true; + } } } break; case 'insert': case 'delete': { + this.blur(currentChange.change); // go to previous change directly const prevChange = changes[changes.indexOf(currentChange.change) - 1]; - if (prevChange) { - return this._revealFirstOrLast(prevChange, false); + if (prevChange && this._revealFirstOrLast(prevChange, false)) { + return true; } } break; @@ -545,23 +598,60 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I integration?.enableAccessibleDiffView(); } } - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.accept(); - this.next(true); + + private getfocusedIntegration(): ChatEditingCodeEditorIntegration | undefined { + const first = this.notebookEditor.getSelectionViewModels()[0]; + if (first) { + return this.cellEditorIntegrations.get(first.model)?.integration; + } + return undefined; } - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.reject(); - this.next(true); + + async acceptNearestChange(hunk: IModifiedFileEntryChangeHunk | undefined): Promise { + if (hunk) { + await hunk.accept(); + } else { + const current = this.getCurrentChange(); + const focused = this.getfocusedIntegration(); + // delete changes can't be focused + if (current && !focused || current?.change.type === 'delete') { + current.change.keep(current?.change.diff.get().changes[current.index]); + } else if (focused) { + await focused.acceptNearestChange(); + } + + this._currentIndex.set(this._currentIndex.get() - 1, undefined); + this.next(true); + } + } + + async rejectNearestChange(hunk: IModifiedFileEntryChangeHunk | undefined): Promise { + if (hunk) { + await hunk.reject(); + } else { + const current = this.getCurrentChange(); + const focused = this.getfocusedIntegration(); + // delete changes can't be focused + if (current && !focused || current?.change.type === 'delete') { + current.change.undo(current.change.diff.get().changes[current.index]); + } else if (focused) { + await focused.rejectNearestChange(); + } + + this._currentIndex.set(this._currentIndex.get() - 1, undefined); + this.next(true); + } + } - async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { - const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; - const diffInput = { - original: { resource: this._entry.originalURI, options: { selection: undefined } }, - modified: { resource: this._entry.modifiedURI, options: { selection: undefined } }, + async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined, _show?: boolean): Promise { + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName; + const diffInput: IResourceDiffEditorInput = { + original: { resource: this._entry.originalURI }, + modified: { resource: this._entry.modifiedURI }, label: defaultAgentName ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) - } satisfies IResourceDiffEditorInput; + }; await this._editorService.openEditor(diffInput); } @@ -625,15 +715,15 @@ export class ChatEditingNotebookDiffEditorIntegration extends Disposable impleme enableAccessibleDiffView(): void { // } - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.accept(); + async acceptNearestChange(change: IModifiedFileEntryChangeHunk): Promise { + await change.accept(); this.next(true); } - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.reject(); + async rejectNearestChange(change: IModifiedFileEntryChangeHunk): Promise { + await change.reject(); this.next(true); } - async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { + async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined, _show?: boolean): Promise { // } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/overlayToolbarDecorator.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/overlayToolbarDecorator.ts new file mode 100644 index 00000000000..48f600848af --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/overlayToolbarDecorator.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { MenuWorkbenchToolBar, HiddenItemStrategy } from '../../../../../../platform/actions/browser/toolbar.js'; +import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { CellEditState, INotebookEditor } from '../../../../notebook/browser/notebookBrowser.js'; +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { CellKind } from '../../../../notebook/common/notebookCommon.js'; +import { IModifiedFileEntryChangeHunk } from '../../../common/chatEditingService.js'; +import { ICellDiffInfo } from './notebookCellChanges.js'; + + +export class OverlayToolbarDecorator extends Disposable { + + private _timeout: any | undefined = undefined; + private readonly overlayDisposables = this._register(new DisposableStore()); + + constructor( + private readonly notebookEditor: INotebookEditor, + private readonly notebookModel: NotebookTextModel, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + ) { + super(); + } + + decorate(changes: ICellDiffInfo[]) { + if (this._timeout !== undefined) { + clearTimeout(this._timeout); + } + this._timeout = setTimeout(() => { + this._timeout = undefined; + this.createMarkdownPreviewToolbars(changes); + }, 100); + } + + private createMarkdownPreviewToolbars(changes: ICellDiffInfo[]) { + this.overlayDisposables.clear(); + + const accessibilitySignalService = this.accessibilitySignalService; + const editor = this.notebookEditor; + for (const change of changes) { + const cellViewModel = this.getCellViewModel(change); + + if (!cellViewModel || cellViewModel.cellKind !== CellKind.Markup) { + continue; + } + const toolbarContainer = document.createElement('div'); + + let overlayId: string | undefined = undefined; + editor.changeCellOverlays((accessor) => { + toolbarContainer.style.right = '44px'; + overlayId = accessor.addOverlay({ + cell: cellViewModel, + domNode: toolbarContainer, + }); + }); + + const removeOverlay = () => { + editor.changeCellOverlays(accessor => { + if (overlayId) { + accessor.removeOverlay(overlayId); + } + }); + }; + + this.overlayDisposables.add({ dispose: removeOverlay }); + + const toolbar = document.createElement('div'); + toolbarContainer.appendChild(toolbar); + toolbar.className = 'chat-diff-change-content-widget'; + toolbar.classList.add('hover'); // Show by default + toolbar.style.position = 'relative'; + toolbar.style.top = '18px'; + toolbar.style.zIndex = '10'; + toolbar.style.display = cellViewModel.getEditState() === CellEditState.Editing ? 'none' : 'block'; + + this.overlayDisposables.add(cellViewModel.onDidChangeState((e) => { + if (e.editStateChanged) { + if (cellViewModel.getEditState() === CellEditState.Editing) { + toolbar.style.display = 'none'; + } else { + toolbar.style.display = 'block'; + } + } + })); + + const scopedInstaService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.notebookEditor.scopedContextKeyService]))); + const toolbarWidget = scopedInstaService.createInstance(MenuWorkbenchToolBar, toolbar, MenuId.ChatEditingEditorHunk, { + telemetrySource: 'chatEditingNotebookHunk', + hiddenItemStrategy: HiddenItemStrategy.NoHide, + toolbarOptions: { primaryGroup: () => true }, + menuOptions: { + renderShortTitle: true, + arg: { + async accept() { + accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); + removeOverlay(); + toolbarWidget.dispose(); + for (const singleChange of change.diff.get().changes) { + await change.keep(singleChange); + } + return true; + }, + async reject() { + accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); + removeOverlay(); + toolbarWidget.dispose(); + for (const singleChange of change.diff.get().changes) { + await change.undo(singleChange); + } + return true; + } + } satisfies IModifiedFileEntryChangeHunk, + }, + }); + + this.overlayDisposables.add(toolbarWidget); + } + } + + private getCellViewModel(change: ICellDiffInfo) { + if (change.type === 'delete' || change.modifiedCellIndex === undefined) { + return undefined; + } + const cell = this.notebookModel.cells[change.modifiedCellIndex]; + const cellViewModel = this.notebookEditor.getViewModel()?.viewCells.find(c => c.handle === cell.handle); + return cellViewModel; + } + + override dispose(): void { + super.dispose(); + if (this._timeout !== undefined) { + clearTimeout(this._timeout); + } + } + +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts new file mode 100644 index 00000000000..3b30754c8ca --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts @@ -0,0 +1,386 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import '../media/simpleBrowserOverlay.css'; +import { combinedDisposable, DisposableMap, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, derivedOpts, observableFromEvent, observableSignalFromEvent } from '../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { localize } from '../../../../../nls.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; +import { EditorGroupView } from '../../../../browser/parts/editor/editorGroupView.js'; +import { Event } from '../../../../../base/common/event.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; +import { isEqual, joinPath } from '../../../../../base/common/resources.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { IHostService } from '../../../../services/host/browser/host.js'; +import { IChatWidgetService, showChatView } from '../chat.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { addDisposableListener } from '../../../../../base/browser/dom.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { cleanupOldImages, createFileForMedia } from '../imageUtils.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IChatRequestVariableEntry } from '../../common/chatModel.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; + +class SimpleBrowserOverlayWidget { + + private readonly _domNode: HTMLElement; + + private readonly imagesFolder: URI; + + private readonly _showStore = new DisposableStore(); + + private _timeout: any | undefined = undefined; + + constructor( + private readonly _editor: IEditorGroup, + private readonly _container: HTMLElement, + @IHostService private readonly _hostService: IHostService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IViewsService private readonly _viewService: IViewsService, + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IPreferencesService private readonly _preferencesService: IPreferencesService, + ) { + + this._showStore.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('chat.sendElementsToChat.enabled')) { + if (this.configurationService.getValue('chat.sendElementsToChat.enabled')) { + this.showElement(this._domNode); + } else { + this.hideElement(this._domNode); + } + } + })); + + this.imagesFolder = joinPath(this.environmentService.workspaceStorageHome, 'vscode-chat-images'); + cleanupOldImages(this.fileService, this.logService, this.imagesFolder); + + this._domNode = document.createElement('div'); + this._domNode.className = 'element-selection-message'; + + const message = document.createElement('span'); + const startSelectionMessage = localize('elementSelectionMessage', 'Add element to chat'); + message.textContent = startSelectionMessage; + this._domNode.appendChild(message); + + let cts: CancellationTokenSource; + const selectButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('selectAnElement', 'Click to select an element.') })); + selectButton.element.className = 'element-selection-start'; + selectButton.label = localize('startSelection', 'Start'); + + const cancelButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('cancelSelection', 'Click to cancel selection.') })); + cancelButton.element.className = 'element-selection-cancel hidden'; + cancelButton.label = localize('cancel', 'Cancel'); + + const configure = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.configureElements', "Configure Attachments Sent") })); + configure.icon = Codicon.gear; + + const collapseOverlay = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.hideOverlay', "Collapse Overlay") })); + collapseOverlay.icon = Codicon.chevronRight; + + const nextSelection = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.nextSelection', "Select Again") })); + nextSelection.icon = Codicon.close; + nextSelection.element.classList.add('hidden'); + + // shown if the overlay is collapsed + const expandOverlay = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.expandOverlay', "Expand Overlay") })); + expandOverlay.icon = Codicon.layout; + const expandContainer = document.createElement('div'); + expandContainer.className = 'element-expand-container hidden'; + expandContainer.appendChild(expandOverlay.element); + this._container.appendChild(expandContainer); + + const resetButtons = () => { + this.hideElement(nextSelection.element); + this.showElement(selectButton.element); + this.showElement(collapseOverlay.element); + }; + + const finishedSelecting = () => { + // stop selection + this.hideElement(cancelButton.element); + this.hideElement(collapseOverlay.element); + this.showElement(nextSelection.element); + + // wait 3 seconds before showing the start button again unless cancelled out. + this._timeout = setTimeout(() => { + message.textContent = startSelectionMessage; + resetButtons(); + }, 3000); + }; + + this._showStore.add(addDisposableListener(selectButton.element, 'click', async () => { + cts = new CancellationTokenSource(); + this._editor.focus(); + + // start selection + message.textContent = localize('elementSelectionInProgress', 'Selecting element...'); + this.hideElement(selectButton.element); + this.showElement(cancelButton.element); + await this.addElementToChat(cts); + + // stop selection + message.textContent = localize('elementSelectionComplete', 'Element added to chat'); + finishedSelecting(); + })); + + this._showStore.add(addDisposableListener(cancelButton.element, 'click', () => { + cts.cancel(); + message.textContent = localize('elementCancelMessage', 'Selection canceled'); + finishedSelecting(); + })); + + this._showStore.add(addDisposableListener(collapseOverlay.element, 'click', () => { + this.hideElement(this._domNode); + this.showElement(expandContainer); + })); + + this._showStore.add(addDisposableListener(expandOverlay.element, 'click', () => { + this.showElement(this._domNode); + this.hideElement(expandContainer); + })); + + this._showStore.add(addDisposableListener(nextSelection.element, 'click', () => { + clearTimeout(this._timeout); + message.textContent = startSelectionMessage; + resetButtons(); + })); + + this._showStore.add(addDisposableListener(configure.element, 'click', () => { + this._preferencesService.openSettings({ jsonEditor: false, query: '@id:chat.sendElementsToChat.enabled,chat.sendElementsToChat.attachCSS,chat.sendElementsToChat.attachImages' }); + })); + } + + hideElement(element: HTMLElement) { + if (element.classList.contains('hidden')) { + return; + } + element.classList.add('hidden'); + } + + showElement(element: HTMLElement) { + if (!element.classList.contains('hidden')) { + return; + } + element.classList.remove('hidden'); + } + + async addElementToChat(cts: CancellationTokenSource) { + const editorContainer = this._container.querySelector('.editor-container') as HTMLDivElement; + const editorContainerPosition = editorContainer ? editorContainer.getBoundingClientRect() : this._container.getBoundingClientRect(); + const elementData = await this._hostService.getElementData(editorContainerPosition, cts.token); + if (!elementData) { + throw new Error('Element data not found'); + } + const bounds = elementData.bounds; + const toAttach: IChatRequestVariableEntry[] = []; + + const widget = this._chatWidgetService.lastFocusedWidget ?? await showChatView(this._viewService); + let value = 'Attached HTML and CSS Context\n\n' + elementData.outerHTML; + if (this.configurationService.getValue('chat.sendElementsToChat.attachCSS')) { + value += '\n\n' + elementData.computedStyle; + } + toAttach.push({ + id: 'element-' + Date.now(), + name: this.getDisplayNameFromOuterHTML(elementData.outerHTML), + fullName: this.getDisplayNameFromOuterHTML(elementData.outerHTML), + value: value, + kind: 'element', + icon: ThemeIcon.fromId(Codicon.layout.id), + }); + + if (this.configurationService.getValue('chat.sendElementsToChat.attachImages')) { + // remove container so we don't block anything on screenshot + this._domNode.style.display = 'none'; + + // Wait 1 extra frame to make sure overlay is gone + await new Promise(resolve => setTimeout(resolve, 100)); + + const screenshot = await this._hostService.getScreenshot(bounds); + if (!screenshot) { + throw new Error('Screenshot failed'); + } + const fileReference = await createFileForMedia(this.fileService, this.imagesFolder, screenshot.buffer, 'image/png'); + toAttach.push({ + id: 'element-screenshot-' + Date.now(), + name: 'Element Screenshot', + fullName: 'Element Screenshot', + kind: 'image', + value: screenshot.buffer, + references: fileReference ? [{ reference: fileReference, kind: 'reference' }] : [], + }); + + this._domNode.style.display = ''; + } + + widget?.attachmentModel?.addContext(...toAttach); + } + + + getDisplayNameFromOuterHTML(outerHTML: string): string { + const firstElementMatch = outerHTML.match(/^<(\w+)([^>]*?)>/); + if (!firstElementMatch) { + throw new Error('No outer element found'); + } + + const tagName = firstElementMatch[1]; + const idMatch = firstElementMatch[2].match(/\s+id\s*=\s*["']([^"']+)["']/i); + const id = idMatch ? `#${idMatch[1]}` : ''; + const classMatch = firstElementMatch[2].match(/\s+class\s*=\s*["']([^"']+)["']/i); + const className = classMatch ? `.${classMatch[1].replace(/\s+/g, '.')}` : ''; + return `${tagName}${id}${className}`; + } + + dispose() { + this._showStore.dispose(); + } + + getDomNode(): HTMLElement { + return this._domNode; + } +} + +class SimpleBrowserOverlayController { + + private readonly _store = new DisposableStore(); + + private readonly _domNode = document.createElement('div'); + + constructor( + container: HTMLElement, + group: IEditorGroup, + @IInstantiationService instaService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + + if (!this.configurationService.getValue('chat.sendElementsToChat.enabled')) { + return; + } + + this._domNode.classList.add('chat-simple-browser-overlay'); + this._domNode.style.position = 'absolute'; + this._domNode.style.bottom = `5px`; + this._domNode.style.right = `5px`; + this._domNode.style.zIndex = `100`; + + const widget = instaService.createInstance(SimpleBrowserOverlayWidget, group, container); + this._domNode.appendChild(widget.getDomNode()); + this._store.add(toDisposable(() => this._domNode.remove())); + this._store.add(widget); + + const show = () => { + if (!container.contains(this._domNode)) { + container.appendChild(this._domNode); + } + }; + + const hide = () => { + if (container.contains(this._domNode)) { + this._domNode.remove(); + } + }; + + const activeEditorSignal = observableSignalFromEvent(this, Event.any(group.onDidActiveEditorChange, group.onDidModelChange)); + + const activeUriObs = derivedOpts({ equalsFn: isEqual }, r => { + + activeEditorSignal.read(r); // signal + + const editor = group.activeEditorPane; + if (editor?.input.editorId === 'mainThreadWebview-simpleBrowser.view') { + const uri = EditorResourceAccessor.getOriginalUri(editor?.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + return uri; + } + return undefined; + }); + + this._store.add(autorun(r => { + + const data = activeUriObs.read(r); + + if (!data) { + hide(); + return; + } + + show(); + })); + } + + dispose(): void { + this._store.dispose(); + } +} + +export class SimpleBrowserOverlay implements IWorkbenchContribution { + + static readonly ID = 'chat.simpleBrowser.overlay'; + + private readonly _store = new DisposableStore(); + + constructor( + @IEditorGroupsService editorGroupsService: IEditorGroupsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + const editorGroups = observableFromEvent( + this, + Event.any(editorGroupsService.onDidAddGroup, editorGroupsService.onDidRemoveGroup), + () => editorGroupsService.groups + ); + + const overlayWidgets = new DisposableMap(); + + this._store.add(autorun(r => { + + const toDelete = new Set(overlayWidgets.keys()); + const groups = editorGroups.read(r); + + + for (const group of groups) { + + if (!(group instanceof EditorGroupView)) { + // TODO@jrieken better with https://github.com/microsoft/vscode/tree/ben/layout-group-container + continue; + } + + toDelete.delete(group); // we keep the widget for this group! + + if (!overlayWidgets.has(group)) { + + const scopedInstaService = instantiationService.createChild( + new ServiceCollection([IContextKeyService, group.scopedContextKeyService]) + ); + + const container = group.element; + + + const ctrl = scopedInstaService.createInstance(SimpleBrowserOverlayController, container, group); + overlayWidgets.set(group, combinedDisposable(ctrl, scopedInstaService)); + } + } + + for (const group of toDelete) { + overlayWidgets.deleteAndDispose(group); + } + })); + } + + dispose(): void { + this._store.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 843bd3bb9a3..927e1a03f0f 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,7 +20,6 @@ import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IChatModel, IExportableChatData, ISerializableChatData } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; -import { IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatMode } from '../common/constants.js'; import { clearChatEditor } from './actions/chatClear.js'; import { ChatEditorInput } from './chatEditorInput.js'; @@ -48,7 +47,6 @@ export class ChatEditor extends EditorPane { @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IChatService private readonly chatService: IChatService, ) { super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } @@ -72,17 +70,16 @@ export class ChatEditor extends EditorPane { autoScroll: mode => mode !== ChatMode.Ask, renderFollowups: true, supportsFileReferences: true, - supportsAdditionalParticipants: true, rendererOptions: { renderTextEditsAsSummary: (uri) => { - return this.chatService.isEditingLocation(ChatAgentLocation.Panel); + return true; }, - referencesExpandedWhenEmptyResponse: !this.chatService.isEditingLocation(ChatAgentLocation.Panel), + referencesExpandedWhenEmptyResponse: false, progressMessageAtBottomOfResponse: mode => mode !== ChatMode.Ask, }, enableImplicitContext: true, - enableWorkingSet: this.chatService.isEditingLocation(ChatAgentLocation.Panel) ? 'explicit' : undefined, - supportsChangingModes: this.chatService.isEditingLocation(ChatAgentLocation.Panel), + enableWorkingSet: 'explicit', + supportsChangingModes: true, }, { listForeground: editorForeground, diff --git a/code/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/code/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index 7f112258424..6dad621002c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -92,7 +92,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler } override get capabilities(): EditorInputCapabilities { - return super.capabilities | EditorInputCapabilities.Singleton; + return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.CanDropIntoEditor; } override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 3752bff4c6b..402f5787097 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -39,7 +39,6 @@ import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/edit import { ExplorerFolderContext } from '../../files/common/files.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatContentInlineReference } from '../common/chatService.js'; -import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatWidgetService } from './chat.js'; import { chatAttachmentResourceContextKey, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js'; @@ -228,14 +227,12 @@ registerAction2(class AddFileToChatAction extends Action2 { override async run(accessor: ServicesAccessor, resource: URI): Promise { const chatWidgetService = accessor.get(IChatWidgetService); - const variablesService = accessor.get(IChatVariablesService); const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - return; - } + if (widget) { + widget.attachmentModel.addFile(resource); - variablesService.attachContext('file', resource, widget.location); + } } }); @@ -365,11 +362,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/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 43b06df7a22..16638fad583 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,22 +7,21 @@ import * as dom from '../../../../base/browser/dom.js'; import { addDisposableListener } from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; -import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { IActionProvider } from '../../../../base/browser/ui/dropdown/dropdown.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction, Separator, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; +import { IAction } from '../../../../base/common/actions.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Emitter } from '../../../../base/common/event.js'; import { HistoryNavigator2 } from '../../../../base/common/history.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; +import { observableFromEvent } from '../../../../base/common/observable.js'; import { isMacintosh } from '../../../../base/common/platform.js'; +import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; @@ -43,12 +42,10 @@ import { SuggestController } from '../../../../editor/contrib/suggest/browser/su import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js'; -import { DropdownMenuActionViewItemWithKeybinding } from '../../../../platform/actions/browser/dropdownActionViewItemWithKeybinding.js'; import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOptions } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; 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 { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -62,7 +59,6 @@ import { WorkbenchList } from '../../../../platform/list/browser/listService.js' import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { ResourceLabels } from '../../../browser/labels.js'; @@ -74,26 +70,25 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingSession } from '../common/chatEditingService.js'; -import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js'; -import { IChatRequestVariableEntry, isImageVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; -import { IChatFollowup, IChatService } from '../common/chatService.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; +import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; -import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; -import { CancelAction, ChatEditingSessionSubmitAction, ChatSubmitAction, ChatSwitchToNextModelActionId, IChatExecuteActionContext, IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; +import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { AttachToolsAction } from './actions/chatToolActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; -import { PromptAttachmentsCollectionWidget } from './attachments/promptAttachments/promptAttachmentsCollectionWidget.js'; +import { PromptInstructionsAttachmentsCollectionWidget } from './attachments/promptInstructions/promptInstructionsCollectionWidget.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; -import { DefaultChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, PasteAttachmentWidget } from './chatAttachmentWidgets.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget } from './chatAttachmentWidgets.js'; 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'; @@ -101,6 +96,8 @@ import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileRefere import { ChatImplicitContext } from './contrib/chatImplicitContext.js'; import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; +import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; +import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js'; const $ = dom.$; @@ -124,6 +121,7 @@ interface IChatInputPartOptions { renderWorkingSet?: boolean; enableImplicitContext?: boolean; supportsChangingModes?: boolean; + dndContainer?: HTMLElement; widgetViewKindTag: string; } @@ -160,10 +158,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly selectedToolsModel: ChatSelectedTools; - public getAttachedAndImplicitContext(sessionId: string): IChatRequestVariableEntry[] { + public async getAttachedAndImplicitContext(sessionId: string): Promise { const contextArr = [...this.attachmentModel.attachments]; if (this.implicitContext?.enabled && this.implicitContext.value) { - contextArr.push(this.implicitContext.toBaseEntry()); + + const implicitChatVariables = await this.implicitContext.toBaseEntries(); + contextArr.push(...implicitChatVariables); } // factor in nested file links of a prompt into the implicit context @@ -182,17 +182,38 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ); } + // prompt files may have nested child references to other prompt + // files that are resolved asynchronously, hence we need to wait + // for the entire prompt instruction tree to be processed + const instructionsStarted = performance.now(); + + // wait for all prompt files resolve precesses to settle + await this.promptInstructionsAttachmentsPart.allSettled(); + + // allow-any-unicode-next-line + this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); + contextArr - .push(...this.instructionAttachmentsPart.chatAttachments); + .push(...this.promptInstructionsAttachmentsPart.chatAttachments); return contextArr; } /** - * Check if the chat input part has any prompt instruction attachments. + * Check if the chat input part has any prompt file attachments. */ - public get hasInstructionAttachments(): boolean { - return !this.instructionAttachmentsPart.empty; + get hasPromptFileAttachments(): boolean { + // if prompt attached explicitly as a "prompt" attachment + if (this.promptInstructionsAttachmentsPart.hasInstructions) { + return true; + } + + if (this.implicitContext === undefined) { + return false; + } + + // if prompt attached as an implicit "current file" context + return (this.implicitContext.isPromptFile && this.implicitContext.enabled); } private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; @@ -245,6 +266,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._editSessionWidgetHeight; } + get attachmentsHeight() { + return this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0); + } + private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -269,25 +294,27 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Context key is set when prompt instructions are attached. */ - private promptInstructionsAttached: IContextKey; + private promptFileAttached: IContextKey; private chatMode: IContextKey; + private modelWidget: ModelPickerActionItem | undefined; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); + private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; get currentLanguageModel() { return this._currentLanguageModel?.identifier; } + get selectedLanguageModel(): ILanguageModelChatMetadataAndIdentifier | undefined { + return this._currentLanguageModel; + } + private _onDidChangeCurrentChatMode = this._register(new Emitter()); readonly onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event; private _currentMode: ChatMode = ChatMode.Ask; public get currentMode(): ChatMode { - if (this.location === ChatAgentLocation.Panel && !this.chatService.unifiedViewEnabled) { - return ChatMode.Ask; - } - return this._currentMode === ChatMode.Agent && !this.agentService.hasToolsAgent ? ChatMode.Edit : this._currentMode; @@ -328,9 +355,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Child widget of prompt instruction attachments. - * See {@linkcode PromptAttachmentsCollectionWidget}. + * See {@linkcode PromptInstructionsAttachmentsCollectionWidget}. */ - private instructionAttachmentsPart: PromptAttachmentsCollectionWidget; + private promptInstructionsAttachmentsPart: PromptInstructionsAttachmentsCollectionWidget; constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used @@ -355,14 +382,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @ILabelService private readonly labelService: ILabelService, @IChatVariablesService private readonly variableService: IChatVariablesService, @IChatAgentService private readonly agentService: IChatAgentService, - @IChatService private readonly chatService: IChatService, @ISharedWebContentExtractorService private readonly sharedWebExtracterService: ISharedWebContentExtractorService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, ) { super(); this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); - this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools)); + this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, observableFromEvent(this, this.onDidChangeCurrentChatMode, () => this.currentMode))); this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this._attachmentModel, styles)); this.getInputState = (): IChatInputState => { @@ -377,11 +403,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); - this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); + this.promptFileAttached = ChatContextKeys.hasPromptFile.bindTo(contextKeyService); this.chatMode = ChatContextKeys.chatMode.bindTo(contextKeyService); this.history = this.loadHistory(); - this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], ChatInputHistoryMaxEntries, historyKeyFn))); + this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '', state: this.getInputState() }], ChatInputHistoryMaxEntries, historyKeyFn))); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { @@ -393,42 +419,62 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); - this.instructionAttachmentsPart = this._register( + this.promptInstructionsAttachmentsPart = this._register( instantiationService.createInstance( - PromptAttachmentsCollectionWidget, + PromptInstructionsAttachmentsCollectionWidget, this.attachmentModel.promptInstructions, this._contextResourceLabels, ), ); // trigger re-layout of chat input when number of instruction attachment changes - this.instructionAttachmentsPart.onAttachmentsCountChange(() => { + this.promptInstructionsAttachmentsPart.onAttachmentsChange(() => { + this._handleAttachedContextChange(); this._onDidChangeHeight.fire(); }); this.initSelectedModel(); + + 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 { return `chat.currentLanguageModel.${this.location}`; } + private getSelectedModelIsDefaultStorageKey(): string { + return `chat.currentLanguageModel.${this.location}.isDefault`; + } + private initSelectedModel() { const persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); + const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'github.copilot-chat/gpt-4o'); + if (persistedSelection) { const model = this.languageModelsService.lookupLanguageModel(persistedSelection); if (model) { - this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection }); - this.checkModelSupported(); + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || model.isDefault) { + this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection }); + this.checkModelSupported(); + } } else { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { const persistedModel = e.added?.find(m => m.identifier === persistedSelection); if (persistedModel) { this._waitForPersistedLanguageModel.clear(); - if (persistedModel.metadata.isUserSelectable) { - this.setCurrentLanguageModel({ metadata: persistedModel.metadata, identifier: persistedSelection }); - this.checkModelSupported(); + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || persistedModel.metadata.isDefault) { + if (persistedModel.metadata.isUserSelectable) { + this.setCurrentLanguageModel({ metadata: persistedModel.metadata, identifier: persistedSelection }); + this.checkModelSupported(); + } } } }); @@ -445,6 +491,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + public switchModel(modelMetadata: Pick) { + const models = this.getModels(); + const model = models.find(m => m.metadata.vendor === modelMetadata.vendor && m.metadata.id === modelMetadata.id && m.metadata.family === modelMetadata.family); + if (model) { + this.setCurrentLanguageModel(model); + } + } + public switchToNextModel(): void { const models = this.getModels(); if (models.length > 0) { @@ -454,6 +508,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + public openModelPicker(): void { + this.modelWidget?.show(); + } + private checkModelSupported(): void { if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { this.setCurrentLanguageModelToDefault(); @@ -465,7 +523,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - mode = validateChatMode(mode) ?? (this.location === ChatAgentLocation.Panel ? ChatMode.Ask : ChatMode.Edit); + mode = validateChatMode(mode) ?? ChatMode.Ask; this._currentMode = mode; this.chatMode.set(mode); this._onDidChangeCurrentChatMode.fire(); @@ -519,6 +577,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefault, StorageScope.APPLICATION, StorageTarget.USER); this._onDidChangeCurrentLanguageModel.fire(model); } @@ -526,7 +585,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private loadHistory(): HistoryNavigator2 { const history = this.historyService.getHistory(this.location); if (history.length === 0) { - history.push({ text: '' }); + history.push({ text: '', state: this.getInputState() }); } return new HistoryNavigator2(history, 50, historyKeyFn); @@ -556,10 +615,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (state.inputState?.chatMode) { this.setChatMode(state.inputState.chatMode); - } else if (this.location === ChatAgentLocation.EditingSession) { - this.setChatMode(ChatMode.Edit); } + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. if (modelIsEmpty) { const storageKey = this.getDefaultModeExperimentStorageKey(); const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false); @@ -666,7 +724,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Check for images in history to restore the value. if (historyAttachments.length > 0) { historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => { - if (attachment.isImage && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { + if (isImageVariableEntry(attachment) && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { const currReference = attachment.references[0].reference; try { const imageBinary = currReference.toString(true).startsWith('http') ? await this.sharedWebExtracterService.readImage(currReference, CancellationToken.None) : (await this.fileService.readFile(currReference)).value; @@ -766,10 +824,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - // A funtion that filters out specifically the `value` property of the attachment. + // A function that filters out specifically the `value` property of the attachment. private getFilteredEntry(query: string, inputState: IChatInputState): IChatHistoryEntry { const attachmentsWithoutImageValues = inputState.chatContextAttachments?.map(attachment => { - if (attachment.isImage && attachment.references?.length && attachment.value) { + if (isImageVariableEntry(attachment) && attachment.references?.length && attachment.value) { const newAttachment = { ...attachment }; newAttachment.value = undefined; return newAttachment; @@ -800,7 +858,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private _handleAttachedContextChange() { - this._hasFileAttachmentContextKey.set(Boolean(this._attachmentModel.attachments.find(a => a.isFile))); + this._hasFileAttachmentContextKey.set(Boolean(this._attachmentModel.attachments.find(a => a.kind === 'file'))); this.renderAttachedContext(); } @@ -855,12 +913,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const attachmentToolbarContainer = elements.attachmentToolbar; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; if (this.options.enableImplicitContext) { - this._implicitContext = this._register(new ChatImplicitContext()); + this._implicitContext = this._register( + this.instantiationService.createInstance(ChatImplicitContext), + ); + this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); } this.renderAttachedContext(); - this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange())); + this._register(this._attachmentModel.onDidChange(() => this._handleAttachedContextChange())); this.renderChatEditingSessionState(null); if (this.options.renderWorkingSet) { @@ -869,7 +930,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.renderChatRelatedFiles(); - this.dnd.addOverlay(container, container); + this.dnd.addOverlay(this.options.dndContainer ?? container, this.options.dndContainer ?? container); const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true); @@ -903,7 +964,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditorElement = dom.append(editorContainer!, $(chatInputEditorContainerSelector)); const editorOptions = getSimpleCodeEditorWidgetOptions(); - editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, CopyPasteController.ID, LinkDetector.ID])); + editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); SuggestController.get(this._inputEditor)?.forceRenderingAbove(); @@ -974,13 +1035,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - if (action.id === ChatSwitchToNextModelActionId && action instanceof MenuItemAction) { + if (action.id === ChatOpenModelPickerActionId && action instanceof MenuItemAction) { if (!this._currentLanguageModel) { this.setCurrentLanguageModelToDefault(); } if (this._currentLanguageModel) { - const itemDelegate: ModelPickerDelegate = { + const itemDelegate: IModelPickerDelegate = { + getCurrentModel: () => this._currentLanguageModel, onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { // The user changed the language model, so we don't wait for the persisted option to be registered @@ -990,14 +1052,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, getModels: () => this.getModels() }; - return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate); + return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, itemDelegate); } } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { const delegate: IModePickerDelegate = { getMode: () => this.currentMode, onDidChangeMode: this._onDidChangeCurrentChatMode.event }; - return this.instantiationService.createInstance(ToggleChatModeActionViewItem, action, delegate); + return this.instantiationService.createInstance(ModePickerActionItem, action, delegate); } return undefined; @@ -1077,11 +1139,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hiddenItemStrategy: HiddenItemStrategy.Ignore, hoverDelegate, actionViewItemProvider: (action, options) => { - if (action.id === 'workbench.action.chat.editing.attachContext' || action.id === 'workbench.action.chat.attachContext') { + if (action.id === 'workbench.action.chat.attachContext') { const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options); return viewItem; } if (action.id === AttachToolsAction.id) { + // TODO@jrieken let's remove this once the tools picker has its final place. return this.selectedToolsModel.toolsActionItemViewItemProvider(action, options); } return undefined; @@ -1108,7 +1171,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const hoverDelegate = store.add(createInstantHoverDelegate()); const attachments = [...this.attachmentModel.attachments.entries()]; - const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty; + const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.promptInstructionsAttachmentsPart.empty; dom.setVisibility(Boolean(hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer); dom.setVisibility(hasAttachments, this.attachedContextContainer); if (!attachments.length) { @@ -1120,8 +1183,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge container.appendChild(implicitPart.domNode); } - this.promptInstructionsAttached.set(!this.instructionAttachmentsPart.empty); - this.instructionAttachmentsPart.render(container); + this.promptFileAttached.set(this.hasPromptFileAttachments); + this.promptInstructionsAttachmentsPart.render(container); for (const [index, attachment] of attachments) { const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; @@ -1129,17 +1192,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const shouldFocusClearButton = index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1); let attachmentWidget; - if (resource && (attachment.isFile || attachment.isDirectory)) { + if (resource && isNotebookOutputVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); - } else if (attachment.isImage) { + } else if (isImageVariableEntry(attachment)) { attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); + } else if (isElementVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else if (isPasteVariableEntry(attachment)) { attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else { attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } store.add(attachmentWidget); - store.add(attachmentWidget.onDidDelete((e) => { + store.add(attachmentWidget.onDidDelete(e => { this.handleAttachmentDeletion(e, index, attachment); })); } @@ -1149,17 +1216,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private handleAttachmentDeletion(e: globalThis.Event, index: number, attachment: IChatRequestVariableEntry) { - this._attachmentModel.delete(attachment.id); - + private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) { // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) if (dom.isKeyboardEvent(e)) { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._indexOfLastAttachedContextDeletedWithKeyboard = index; - } + this._indexOfLastAttachedContextDeletedWithKeyboard = index; } + this._attachmentModel.delete(attachment.id); + if (this._attachmentModel.size === 0) { this.focus(); } @@ -1231,7 +1295,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; @@ -1263,7 +1327,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); if (pane) { - entry?.getEditorIntegration(pane).reveal(true); + entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus); } } })); @@ -1406,7 +1470,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28, - attachmentsHeight: this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0), + attachmentsHeight: this.attachmentsHeight, editorBorder: 2, inputPartHorizontalPaddingInside: 12, toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0, @@ -1476,177 +1540,28 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { } } -interface ModelPickerDelegate { - onDidChangeModel: Event; - setModel(selectedModelId: ILanguageModelChatMetadataAndIdentifier): void; - getModels(): ILanguageModelChatMetadataAndIdentifier[]; -} - -class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding { - constructor( - action: MenuItemAction, - private currentLanguageModel: ILanguageModelChatMetadataAndIdentifier, - private readonly delegate: ModelPickerDelegate, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatEntitlementService chatEntitlementService: IChatEntitlementService, - @ICommandService commandService: ICommandService, - @IMenuService menuService: IMenuService, - @ITelemetryService telemetryService: ITelemetryService, - ) { - const modelActionsProvider: IActionProvider = { - getActions: () => { - const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { - return { - id: entry.identifier, - label: entry.metadata.name, - tooltip: '', - class: undefined, - enabled: true, - checked: entry.identifier === this.currentLanguageModel.identifier, - run: () => { - this.currentLanguageModel = entry; - this.renderLabel(this.element!); - this.delegate.setModel(entry); - } - }; - }; - - const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); - const actions = models.map(entry => setLanguageModelAction(entry)); - - // Add menu contributions from extensions - const menuActions = menuService.getMenuActions(MenuId.ChatModelPicker, contextKeyService); - const menuContributions = getFlatActionBarActions(menuActions); - if (menuContributions.length > 0 || chatEntitlementService.entitlement === ChatEntitlement.Limited) { - actions.push(new Separator()); - } - actions.push(...menuContributions); - if (chatEntitlementService.entitlement === ChatEntitlement.Limited) { - actions.push(toAction({ - id: 'moreModels', label: localize('chat.moreModels', "Add more Models"), run: () => { - const commandId = 'workbench.action.chat.upgradePlan'; - telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-models' }); - commandService.executeCommand(commandId); - } - })); - } - return actions; - } - }; - - const actionWithLabel: IAction = { - ...action, - tooltip: localize('chat.modelPicker.label', "Pick Model"), - run: () => { } - }; - super(actionWithLabel, modelActionsProvider, contextMenuService, undefined, keybindingService, contextKeyService); - this._register(delegate.onDidChangeModel(modelId => { - this.currentLanguageModel = modelId; - this.renderLabel(this.element!); - })); - } - - protected override renderLabel(element: HTMLElement): IDisposable | null { - this.setAriaLabelAttributes(element); - dom.reset(element, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); - return null; - } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-modelPicker-item'); - } -} - const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); -interface IModePickerDelegate { - onDidChangeMode: Event; - getMode(): ChatMode; -} +class AddFilesButton extends ActionViewItem { -class ToggleChatModeActionViewItem extends DropdownMenuActionViewItemWithKeybinding { - constructor( - action: MenuItemAction, - private readonly delegate: IModePickerDelegate, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatService chatService: IChatService, - @IChatAgentService chatAgentService: IChatAgentService, - ) { - const makeAction = (mode: ChatMode): IAction => ({ - ...action, - id: mode, - label: this.modeToString(mode), - class: undefined, - enabled: true, - checked: delegate.getMode() === mode, - run: async () => { - const result = await action.run({ mode } satisfies IToggleChatModeArgs); - this.renderLabel(this.element!); - return result; - } + constructor(context: unknown, action: IAction, options: IActionViewItemOptions) { + super(context, action, { + ...options, + icon: false, + label: true, + keybindingNotRenderedWithLabel: true, }); - - const actionProvider: IActionProvider = { - getActions: () => { - const agentStateActions = [ - makeAction(ChatMode.Edit), - ]; - if (chatAgentService.hasToolsAgent) { - agentStateActions.push(makeAction(ChatMode.Agent)); - } - - if (chatService.unifiedViewEnabled) { - agentStateActions.unshift(makeAction(ChatMode.Ask)); - } - - return agentStateActions; - } - }; - - super(action, actionProvider, contextMenuService, undefined, keybindingService, contextKeyService); - this._register(delegate.onDidChangeMode(() => this.renderLabel(this.element!))); - } - - private modeToString(mode: ChatMode) { - switch (mode) { - case ChatMode.Agent: - return localize('chat.agentMode', "Agent"); - case ChatMode.Edit: - return localize('chat.normalMode', "Edit"); - case ChatMode.Ask: - default: - return localize('chat.askMode', "Ask"); - } - } - - protected override renderLabel(element: HTMLElement): IDisposable | null { - // Can't call super.renderLabel because it has a hack of forcing the 'codicon' class - this.setAriaLabelAttributes(element); - - const state = this.modeToString(this.delegate.getMode()); - dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); - return null; } override render(container: HTMLElement): void { + container.classList.add('chat-attachment-button'); super.render(container); - container.classList.add('chat-modelPicker-item'); - } -} - -class AddFilesButton extends ActionViewItem { - constructor(context: unknown, action: IAction, options: IActionViewItemOptions) { - super(context, action, options); } - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-attached-context-attachment', 'chat-add-files'); + protected override updateLabel(): void { + assertType(this.label); + const message = `$(attach) ${this.action.label}`; + dom.reset(this.label, ...renderLabelWithIcons(message)); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 03756411548..b966a950be8 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -48,11 +48,11 @@ import { IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatService, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { getNWords } from '../common/chatWordCounter.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; -import { ChatMode } from '../common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../common/constants.js'; import { MarkUnhelpfulActionId } from './actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; @@ -62,6 +62,7 @@ import { ChatCodeCitationContentPart } from './chatContentParts/chatCodeCitation import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandContentPart.js'; import { ChatConfirmationContentPart } from './chatContentParts/chatConfirmationContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts/chatContentParts.js'; +import { ChatExtensionsContentPart } from './chatContentParts/chatExtensionsContentPart.js'; import { ChatMarkdownContentPart, EditorPool } from './chatContentParts/chatMarkdownContentPart.js'; import { ChatProgressContentPart, ChatWorkingProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart.js'; @@ -165,7 +166,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer element.model.setPaused(p) }); } return { content: partsToRender, moreContentAvailable }; @@ -839,7 +839,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !part.isComplete))) { + ((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !part.isComplete)) || + (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) + ) { return true; } @@ -902,6 +904,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); + return part; + } + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { if (!isResponseVM(context.element)) { return; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/code/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 3b763727a52..d87cc9c4afa 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -20,7 +20,7 @@ import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { contentRefUrl } from '../common/annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../common/chatColors.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; import { IChatMarkdownContent, IChatService } from '../common/chatService.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; @@ -112,8 +112,9 @@ export class ChatMarkdownDecorationsRenderer { const title = uri ? this.labelService.getUriLabel(uri, { relative: true }) : part instanceof ChatRequestSlashCommandPart ? part.slashCommand.detail : part instanceof ChatRequestAgentSubcommandPart ? part.command.description : - part instanceof ChatRequestToolPart ? (this.toolsService.getTool(part.toolId)?.userDescription) : - ''; + part instanceof ChatRequestSlashPromptPart ? part.slashPromptCommand.command : + part instanceof ChatRequestToolPart ? (this.toolsService.getTool(part.toolId)?.userDescription) : + ''; const args: IDecorationWidgetArgs = { title }; const text = part.text; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/code/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts index 6e78f992eeb..858bc89d5f5 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { $ } from '../../../../base/browser/dom.js'; import { MarkdownRenderOptions, MarkedOptions } from '../../../../base/browser/markdownRenderer.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; @@ -15,7 +16,6 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../files/browser/fileConstants.js'; -import { ITrustedDomainService } from '../../url/browser/trustedDomainService.js'; const allowedHtmlTags = [ 'b', @@ -62,7 +62,6 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { options: IMarkdownRendererOptions | undefined, @ILanguageService languageService: ILanguageService, @IOpenerService openerService: IOpenerService, - @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, @IHoverService private readonly hoverService: IHoverService, @IFileService private readonly fileService: IFileService, @ICommandService private readonly commandService: ICommandService, @@ -73,7 +72,7 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { options = { ...options, - remoteImageIsAllowed: (uri) => this.trustedDomainService.isValid(uri), + remoteImageIsAllowed: (_uri) => false, sanitizerOptions: { replaceWithPlaintext: true, allowedTags: allowedHtmlTags, @@ -90,6 +89,14 @@ export class ChatMarkdownRenderer extends MarkdownRenderer { } : markdown; const result = super.render(mdWithBody, options, markedOptions); + + // In some cases, the renderer can return text that is not inside a

, + // but our CSS expects text to be in a

for margin to be applied properly. + // So just normalize it. + const lastChild = result.element.lastChild; + if (lastChild?.nodeType === Node.TEXT_NODE && lastChild.textContent?.trim()) { + lastChild.replaceWith($('p', undefined, lastChild.textContent)); + } return this.attachCustomHover(result); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/code/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index e794b9ee88f..ed49ba1a4db 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -11,36 +11,27 @@ 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 { 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 { ContextKeyExpr, IContextKeyService } 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, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IWorkbenchContribution } 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, showChatView } from './chat.js'; -import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js'; +import { ChatAgentLocation, ChatMode } from '../common/constants.js'; +import { ChatViewId } from './chat.js'; +import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js'; // --- Chat Container & View Registration @@ -76,7 +67,10 @@ const chatViewDescriptor: IViewDescriptor[] = [{ }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( - ChatContextKeys.Setup.hidden.negate(), + ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate() // do not pretend a working Chat view if extension is explicitly disabled + ), ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid @@ -84,18 +78,6 @@ const chatViewDescriptor: IViewDescriptor[] = [{ }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer); -// --- Edits Container & View Registration - -const editsViewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: CHAT_EDITING_SIDEBAR_PANEL_ID, - title: localize2('chatEditing.viewContainer.label', "Copilot Edits"), - icon: Codicon.editSession, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), - storageId: CHAT_EDITING_SIDEBAR_PANEL_ID, - hideIfEmpty: true, - order: 101, -}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true }); - const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatParticipants', jsonSchema: { @@ -259,7 +241,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if ((providerDescriptor.isDefault || providerDescriptor.isAgent) && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { + if ((providerDescriptor.isDefault || providerDescriptor.modes) && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); continue; } @@ -305,10 +287,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { name: providerDescriptor.name, fullName: providerDescriptor.fullName, isDefault: providerDescriptor.isDefault, - isToolsAgent: providerDescriptor.isAgent, locations: isNonEmptyArray(providerDescriptor.locations) ? providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], + modes: providerDescriptor.modes ?? [ChatMode.Ask], slashCommands: providerDescriptor.commands ?? [], disambiguation: coalesce(participantsDisambiguation.flat()), } satisfies IChatAgentData)); @@ -431,143 +413,3 @@ 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/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 72911840c5f..37a86941f7c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; @@ -24,7 +23,7 @@ import { IExtensionService, isProposedApiEnabled } from '../../../services/exten import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; import { IChatWidgetService } from './chat.js'; import { ChatInputPart } from './chatInputPart.js'; -import { resizeImage } from './imageUtils.js'; +import { cleanupOldImages, createFileForMedia, resizeImage } from './imageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; @@ -50,7 +49,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider { @ILogService private readonly logService: ILogService, ) { this.imagesFolder = joinPath(this.environmentService.workspaceStorageHome, 'vscode-chat-images'); - this.cleanupOldImages(); + cleanupOldImages(this.fileService, this.logService, this.imagesFolder,); } async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { @@ -100,7 +99,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider { tempDisplayName = `${displayName} ${appendValue}`; } - const fileReference = await this.createFileForMedia(currClipboard, mimeType); + const fileReference = await createFileForMedia(this.fileService, this.imagesFolder, currClipboard, mimeType); if (token.isCancellationRequested || !fileReference) { return; } @@ -126,57 +125,6 @@ export class PasteImageProvider implements DocumentPasteEditProvider { const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); return createEditSession(edit); } - - private async createFileForMedia( - dataTransfer: Uint8Array, - mimeType: string, - ): Promise { - const exists = await this.fileService.exists(this.imagesFolder); - if (!exists) { - await this.fileService.createFolder(this.imagesFolder); - } - - const ext = mimeType.split('/')[1] || 'png'; - const filename = `image-${Date.now()}.${ext}`; - const fileUri = joinPath(this.imagesFolder, filename); - - const buffer = VSBuffer.wrap(dataTransfer); - await this.fileService.writeFile(fileUri, buffer); - - return fileUri; - } - - private async cleanupOldImages(): Promise { - const exists = await this.fileService.exists(this.imagesFolder); - if (!exists) { - return; - } - - const duration = 7 * 24 * 60 * 60 * 1000; // 7 days - const files = await this.fileService.resolve(this.imagesFolder); - if (!files.children) { - return; - } - - await Promise.all(files.children.map(async (file) => { - try { - const timestamp = this.getTimestampFromFilename(file.name); - if (timestamp && (Date.now() - timestamp > duration)) { - await this.fileService.del(file.resource); - } - } catch (err) { - this.logService.error('Failed to clean up old images', err); - } - })); - } - - private getTimestampFromFilename(filename: string): number | undefined { - const match = filename.match(/image-(\d+)\./); - if (match) { - return parseInt(match[1], 10); - } - return undefined; - } } async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string, resource: URI): Promise { @@ -190,7 +138,6 @@ async function getImageAttachContext(data: Uint8Array, mimeType: string, token: value: data, id: imageHash, name: displayName, - isImage: true, icon: Codicon.fileMedia, mimeType, isPasted: true, diff --git a/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 5c96d139e1a..72b81354148 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -5,6 +5,7 @@ import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { stripIcons } from '../../../../base/common/iconLabels.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -32,7 +33,6 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation const verifiedWidget: IChatWidget = widget; const focusedItem = verifiedWidget.getFocus(); - if (!focusedItem) { return; } @@ -65,6 +65,19 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi if (!responseContent && 'errorDetails' in item && item.errorDetails) { responseContent = item.errorDetails.message; } + if (isResponseVM(item)) { + const toolInvocation = item.response.value.find(item => item.kind === 'toolInvocation'); + if (toolInvocation?.confirmationMessages) { + const title = toolInvocation.confirmationMessages.title; + const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderMarkdownAsPlaintext(toolInvocation.confirmationMessages.message)); + const terminalCommand = toolInvocation.toolSpecificData && 'command' in toolInvocation.toolSpecificData ? toolInvocation.toolSpecificData.command : undefined; + responseContent += `${title}`; + if (terminalCommand) { + responseContent += `: ${terminalCommand}`; + } + responseContent += `\n${message}`; + } + } return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/code/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts index 73541fb8063..8564170f9b7 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { reset } from '../../../../base/browser/dom.js'; import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; @@ -19,6 +18,7 @@ import { MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ChatMode } from '../common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../common/languageModelToolsService.js'; /** @@ -43,7 +43,10 @@ export class ChatSelectedTools extends Disposable { readonly toolsActionItemViewItemProvider: IActionViewItemProvider & { onDidRender: Event }; + private readonly _allTools: IObservable[]>; + constructor( + mode: IObservable, @ILanguageModelToolsService toolsService: ILanguageModelToolsService, @IInstantiationService instaService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -52,10 +55,7 @@ export class ChatSelectedTools extends Disposable { this._selectedTools = this._register(storedTools(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService)); - const allTools = observableFromEvent( - toolsService.onDidChangeTools, - () => Array.from(toolsService.getTools()).filter(t => t.supportsToolPicker) - ); + this._allTools = observableFromEvent(toolsService.onDidChangeTools, () => Array.from(toolsService.getTools())); const disabledData = this._selectedTools.map(data => { return (data.disabledBuckets?.length || data.disabledTools?.length) && { @@ -65,19 +65,21 @@ export class ChatSelectedTools extends Disposable { }); this.tools = derived(r => { + const tools = this._allTools.read(r); + if (mode.read(r) !== ChatMode.Agent) { + return tools; + } const disabled = disabledData.read(r); - const tools = allTools.read(r); if (!disabled) { return tools; } - return tools.filter(t => !(disabled.toolIds.has(t.id) || disabled.buckets.has(ToolDataSource.toKey(t.source))) ); }); const toolsCount = derived(r => { - const count = allTools.read(r).length; + const count = this._allTools.read(r).length; const enabled = this.tools.read(r).length; return { count, enabled }; }); @@ -95,7 +97,7 @@ export class ChatSelectedTools extends Disposable { override render(container: HTMLElement): void { this.options.icon = false; this.options.label = true; - container.classList.add('chat-mcp'); + container.classList.add('chat-mcp', 'chat-attachment-button'); super.render(container); } @@ -112,6 +114,7 @@ export class ChatSelectedTools extends Disposable { : localize('tool.0', "{0} {1}", '$(tools)', count); reset(this.label, ...renderLabelWithIcons(message)); + if (this.element?.isConnected) { onDidRender.fire(); } @@ -124,10 +127,29 @@ export class ChatSelectedTools extends Disposable { ); } + selectOnly(toolIds: readonly string[]): void { + const uniqueTools = new Set(toolIds); + + const disabledTools = this._allTools.get().filter(tool => !uniqueTools.has(tool.id)); + + this.update([], disabledTools); + } + update(disableBuckets: readonly ToolDataSource[], disableTools: readonly IToolData[]): void { this._selectedTools.set({ disabledBuckets: disableBuckets.map(ToolDataSource.toKey), disabledTools: disableTools.map(t => t.id) }, undefined); } + + asEnablementMap(): Map { + const result = new Map(); + const enabledTools = new Set(this.tools.get().map(t => t.id)); + for (const tool of this._allTools.get()) { + if (tool.supportsToolPicker) { + result.set(tool, enabledTools.has(tool.id)); + } + } + return result; + } } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 8a8a90720c9..043193f16d9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import './media/chatSetup.css'; -import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; -import { ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; -import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { mainWindow } from '../../../../base/browser/window.js'; +import { $ } from '../../../../base/browser/dom.js'; +import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { timeout } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { combinedDisposable, Disposable, DisposableStore, IDisposable, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js'; import Severity from '../../../../base/common/severity.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; @@ -31,8 +30,11 @@ import { ConfigurationTarget, IConfigurationService } from '../../../../platform import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; @@ -41,7 +43,6 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; @@ -52,24 +53,20 @@ import { nullExtensionDescription } from '../../../services/extensions/common/ex import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent } from '../common/chatAgents.js'; +import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../common/chatEntitlementService.js'; +import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestToolEntry, IChatRequestVariableData } from '../common/chatModel.js'; +import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; import { IChatProgress, IChatService } from '../common/chatService.js'; -import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js'; -import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, IChatWidgetService, preferCopilotEditsView, showCopilotView } from './chat.js'; -import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; -import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelsService } from '../common/languageModels.js'; -import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; -import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { createWorkbenchDialogOptions } from '../../../../platform/dialogs/browser/dialog.js'; -import { IChatRequestModel } from '../common/chatModel.js'; +import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js'; +import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js'; +import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -79,6 +76,7 @@ const defaultChat = { privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', + manageOveragesUrl: product.defaultChatAgent?.manageOverageUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', providerName: product.defaultChatAgent?.providerName ?? '', enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', @@ -92,47 +90,35 @@ const defaultChat = { chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '', }; +const copilotSettingsMessage = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "Copilot Free and Pro may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); + //#region Contribution -const ToolsAgentWhen = ContextKeyExpr.and( +const ToolsAgentContextKey = ContextKeyExpr.and( ContextKeyExpr.equals(`config.${ChatConfiguration.AgentEnabled}`, true), ChatContextKeys.Editing.agentModeDisallowed.negate(), ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension ); -class SetupChatAgentImplementation extends Disposable implements IChatAgentImplementation { +class SetupAgent extends Disposable implements IChatAgentImplementation { - static register(instantiationService: IInstantiationService, location: ChatAgentLocation, isToolsAgent: boolean, context: ChatEntitlementContext, controller: Lazy): { disposable: IDisposable; agent: SetupChatAgentImplementation } { + static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { agent: SetupAgent; disposable: IDisposable } { return instantiationService.invokeFunction(accessor => { const chatAgentService = accessor.get(IChatAgentService); let id: string; let description = localize('chatDescription', "Ask Copilot"); - let welcomeMessageContent: IChatWelcomeMessageContent | undefined; - const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); switch (location) { case ChatAgentLocation.Panel: - id = 'setup.chat'; - welcomeMessageContent = { - title: description, - message: new MarkdownString(baseMessage), - icon: Codicon.copilotLarge - }; - break; - case ChatAgentLocation.EditingSession: - id = isToolsAgent ? 'setup.agent' : 'setup.edits'; - description = isToolsAgent ? localize('agentDescription', "Edit files in your workspace in agent mode") : localize('editsDescription', "Edit files in your workspace"); - welcomeMessageContent = isToolsAgent ? - { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", `https://aka.ms/vscode-copilot-agent`) + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge - } : - { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.") + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge - }; + if (mode === ChatMode.Ask) { + id = 'setup.chat'; + } else if (mode === ChatMode.Edit) { + id = 'setup.edits'; + description = localize('editsDescription', "Edit files in your workspace"); + } else { + id = 'setup.agent'; + description = localize('agentDescription', "Edit files in your workspace in agent mode"); + } break; case ChatAgentLocation.Terminal: id = 'setup.terminal'; @@ -145,40 +131,70 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple break; } - const disposable = new DisposableStore(); - - disposable.add(chatAgentService.registerAgent(id, { - id, - name: `${defaultChat.providerName} Copilot`, - isDefault: true, - isCore: true, - isToolsAgent, - when: isToolsAgent ? ToolsAgentWhen?.serialize() : undefined, - slashCommands: [], - disambiguation: [], - locations: [location], - metadata: { - welcomeMessageContent, - helpTextPrefix: SetupChatAgentImplementation.SETUP_NEEDED_MESSAGE - }, - description, - extensionId: nullExtensionDescription.identifier, - extensionDisplayName: nullExtensionDescription.name, - extensionPublisherId: nullExtensionDescription.publisher - })); + return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.providerName} Copilot`, true, description, location, mode, context, controller); + }); + } + + static registerVSCodeAgent(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy): { agent: SetupAgent; disposable: IDisposable } { + return instantiationService.invokeFunction(accessor => { + const chatAgentService = accessor.get(IChatAgentService); - const agent = disposable.add(instantiationService.createInstance(SetupChatAgentImplementation, context, controller, location)); - disposable.add(chatAgentService.registerAgentImplementation(id, agent)); + const disposables = new DisposableStore(); - return { agent, disposable }; + const { agent, disposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Panel, undefined, context, controller); + disposables.add(disposable); + + disposables.add(SetupTool.registerTool(instantiationService, { + id: 'setup.tools.createNewWorkspace', + source: { + type: 'internal', + }, + icon: Codicon.newFolder, + displayName: localize('setupToolDisplayName', "New Workspace"), + modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), + userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), + canBeReferencedInPrompt: true, + toolReferenceName: 'new', + when: ContextKeyExpr.true(), + supportsToolPicker: true, + }).disposable); + + return { agent, disposable: disposables }; }); } + private static doRegisterAgent(instantiationService: IInstantiationService, chatAgentService: IChatAgentService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { agent: SetupAgent; disposable: IDisposable } { + const disposables = new DisposableStore(); + disposables.add(chatAgentService.registerAgent(id, { + id, + name, + isDefault, + isCore: true, + modes: mode ? [mode] : [ChatMode.Ask], + when: mode === ChatMode.Agent ? ToolsAgentContextKey?.serialize() : undefined, + slashCommands: [], + disambiguation: [], + locations: [location], + metadata: { helpTextPrefix: SetupAgent.SETUP_NEEDED_MESSAGE }, + description, + extensionId: nullExtensionDescription.identifier, + extensionDisplayName: nullExtensionDescription.name, + extensionPublisherId: nullExtensionDescription.publisher + })); + + const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location)); + disposables.add(chatAgentService.registerAgentImplementation(id, agent)); + + return { agent, disposable: disposables }; + } + private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up Copilot to use Chat.")); 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, @@ -192,25 +208,26 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void): Promise { - return this.instantiationService.invokeFunction(async accessor => { - const chatService = accessor.get(IChatService); // use accessor for lazy loading - const languageModelsService = accessor.get(ILanguageModelsService); // of chat related services + return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => { + const chatService = accessor.get(IChatService); + const languageModelsService = accessor.get(ILanguageModelsService); const chatWidgetService = accessor.get(IChatWidgetService); const chatAgentService = accessor.get(IChatAgentService); + const languageModelToolsService = accessor.get(ILanguageModelToolsService); - return this.doInvoke(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + return this.doInvoke(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); }); } - private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) { - return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); } - return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); } - private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1); if (!requestModel) { this.logService.error('[chat setup] Request model not found, cannot redispatch request.'); @@ -222,28 +239,51 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple content: new MarkdownString(localize('waitingCopilot', "Getting Copilot ready.")), }); - await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); return {}; } - private _handlingForwardedRequest: string | undefined; - private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { + private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { + try { + await this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); + } catch (error) { + progress({ + kind: 'warning', + content: new MarkdownString(localize('copilotUnavailableWarning', "Copilot failed to get a response. Please try again.")) + }); + } + } + + private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { + if (this.pendingForwardedRequests.has(requestModel.session.sessionId)) { + throw new Error('Request already in progress'); + } + + const forwardRequest = this.doForwardRequestToCopilotWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); + this.pendingForwardedRequests.set(requestModel.session.sessionId, forwardRequest); - if (this._handlingForwardedRequest === requestModel.message.text) { - throw new Error('Already handling this request'); + try { + await forwardRequest; + } finally { + this.pendingForwardedRequests.delete(requestModel.session.sessionId); } + } - this._handlingForwardedRequest = requestModel.message.text; + private async doForwardRequestToCopilotWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { + const widget = chatWidgetService.getWidgetBySessionId(requestModel.session.sessionId); + const mode = widget?.input.currentMode; + const languageModel = widget?.input.currentLanguageModel; // We need a signal to know when we can resend the request to // Copilot. Waiting for the registration of the agent is not - // enough, we also need a language model to be available. + // enough, we also need a language/tools model to be available. + const whenAgentReady = this.whenAgentReady(chatAgentService, mode); const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService); - const whenAgentReady = this.whenAgentReady(chatAgentService); + const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel); - if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise) { + if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) { const timeoutHandle = setTimeout(() => { progress({ kind: 'progressMessage', @@ -254,13 +294,17 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple try { const ready = await Promise.race([ timeout(20000).then(() => 'timedout'), - Promise.allSettled([whenLanguageModelReady, whenAgentReady]) + this.whenDefaultAgentFailed(chatService).then(() => 'error'), + Promise.allSettled([whenLanguageModelReady, whenAgentReady, whenToolsModelReady]) ]); - 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 review the guidance in the Chat view.") : + localize('copilotFailedWarning', "Copilot failed to get ready. Please review the guidance in the Chat view.") + ) }); // This means Copilot is unhealthy and we cannot retry the @@ -273,11 +317,7 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } } - const widget = chatWidgetService.getWidgetBySessionId(requestModel.session.sessionId); - chatService.resendRequest(requestModel, { - mode: widget?.input.currentMode, - userSelectedModelId: widget?.input.currentLanguageModel, - }); + await chatService.resendRequest(requestModel, { mode, userSelectedModelId: languageModel }); } private whenLanguageModelReady(languageModelsService: ILanguageModelsService): Promise | void { @@ -291,19 +331,49 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, e => e.added?.some(added => added.metadata.isDefault) ?? false)); } - private whenAgentReady(chatAgentService: IChatAgentService): Promise | void { - const defaultAgent = chatAgentService.getDefaultAgent(this.location); + private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise | void { + const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart); + if (!needsToolsModel) { + return; // No tools in this request, no need to check + } + + // check that tools other than setup. and internal tools are registered. + for (const tool of languageModelToolsService.getTools()) { + if (tool.source.type !== 'internal') { + return; // we have tools! + } + } + + return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => { + for (const tool of languageModelToolsService.getTools()) { + if (tool.source.type !== 'internal') { + return true; // we have tools! + } + } + + return false; // no external tools found + })); + } + + private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatMode | undefined): Promise | void { + const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode); if (defaultAgent && !defaultAgent.isCore) { return; // we have a default agent from an extension! } return Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => { - const defaultAgent = chatAgentService.getDefaultAgent(this.location); + const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode); return Boolean(defaultAgent && !defaultAgent.isCore); })); } - private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + 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, languageModelToolsService: ILanguageModelToolsService): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' }); const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1); @@ -325,9 +395,9 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } })); - let success = undefined; + let result: IChatSetupResult | undefined = undefined; try { - success = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run(); + result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run(); } catch (error) { this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`); } finally { @@ -335,10 +405,18 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } // User has agreed to run the setup - if (typeof success === 'boolean') { - if (success) { - if (requestModel) { - await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + if (typeof result?.success === 'boolean') { + if (result.success) { + if (result.dialogSkipped) { + progress({ + kind: 'markdownContent', + content: new MarkdownString([localize('copilotSetupSuccess', "Copilot setup finished successfully."), copilotSettingsMessage].join('\n\n')) + }); + } else if (requestModel) { + let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); // Replace agent part with the actual Copilot agent... + newRequest = this.replaceToolInRequestModel(newRequest); // ...then replace any tool parts with the actual Copilot tools + + await this.forwardRequestToCopilot(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); } } else { progress({ @@ -352,12 +430,132 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple else { progress({ kind: 'markdownContent', - content: SetupChatAgentImplementation.SETUP_NEEDED_MESSAGE, + content: SetupAgent.SETUP_NEEDED_MESSAGE, }); } return {}; } + + private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel { + const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + if (!agentPart) { + return requestModel; + } + + const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase()); + const githubAgent = chatAgentService.getAgent(agentId); + if (!githubAgent) { + return requestModel; + } + + const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent); + + return new ChatRequestModel({ + session: requestModel.session as ChatModel, + message: { + parts: requestModel.message.parts.map(part => { + if (part instanceof ChatRequestAgentPart) { + return newAgentPart; + } + return part; + }), + text: requestModel.message.text + }, + variableData: requestModel.variableData, + timestamp: Date.now(), + attempt: requestModel.attempt, + confirmation: requestModel.confirmation, + locationData: requestModel.locationData, + attachedContext: requestModel.attachedContext, + isCompleteAddedRequest: requestModel.isCompleteAddedRequest, + }); + } + + private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel { + const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart); + if (!toolPart) { + return requestModel; + } + + const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase()); + const newToolPart = new ChatRequestToolPart( + toolPart.range, + toolPart.editorRange, + toolPart.toolName, + toolId, + toolPart.displayName, + toolPart.icon + ); + + const chatRequestToolEntry: IChatRequestToolEntry = { + id: toolId, + name: 'new', + range: toolPart.range, + kind: 'tool', + value: undefined + }; + + const variableData: IChatRequestVariableData = { + variables: [chatRequestToolEntry] + }; + + return new ChatRequestModel({ + session: requestModel.session as ChatModel, + message: { + parts: requestModel.message.parts.map(part => { + if (part instanceof ChatRequestToolPart) { + return newToolPart; + } + return part; + }), + text: requestModel.message.text + }, + variableData: variableData, + timestamp: Date.now(), + attempt: requestModel.attempt, + confirmation: requestModel.confirmation, + locationData: requestModel.locationData, + attachedContext: [chatRequestToolEntry], + isCompleteAddedRequest: requestModel.isCompleteAddedRequest, + }); + } +} + + +class SetupTool extends Disposable implements IToolImpl { + + static registerTool(instantiationService: IInstantiationService, toolData: IToolData): { tool: SetupTool; disposable: IDisposable } { + return instantiationService.invokeFunction(accessor => { + const toolService = accessor.get(ILanguageModelToolsService); + + const disposables = new DisposableStore(); + + disposables.add(toolService.registerToolData(toolData)); + + const tool = instantiationService.createInstance(SetupTool); + disposables.add(toolService.registerToolImplementation(toolData.id, tool)); + + return { tool, disposable: disposables }; + }); + } + + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise { + const result: IToolResult = { + content: [ + { + kind: 'text', + value: '' + } + ] + }; + + return result; + } + + async prepareToolInvocation?(parameters: any, token: CancellationToken): Promise { + return undefined; + } } enum ChatSetupStrategy { @@ -367,6 +565,11 @@ enum ChatSetupStrategy { SetupWithEnterpriseProvider = 3 } +interface IChatSetupResult { + readonly success: boolean | undefined; + readonly dialogSkipped: boolean; +} + class ChatSetup { private static instance: ChatSetup | undefined = undefined; @@ -374,14 +577,16 @@ class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IContextMenuService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService)); + return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IContextMenuService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService)); }); } return instance; } - private pendingRun: Promise | undefined = undefined; + private pendingRun: Promise | undefined = undefined; + + private skipDialogOnce = false; private constructor( private readonly context: ChatEntitlementContext, @@ -393,9 +598,14 @@ class ChatSetup { @IKeybindingService private readonly keybindingService: IKeybindingService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { } - async run(): Promise { + skipDialog(): void { + this.skipDialogOnce = true; + } + + async run(): Promise { if (this.pendingRun) { return this.pendingRun; } @@ -409,25 +619,35 @@ class ChatSetup { } } - private async doRun(): Promise { + private async doRun(): Promise { + const dialogSkipped = this.skipDialogOnce; + this.skipDialogOnce = false; + let setupStrategy: ChatSetupStrategy; - if (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { + if (dialogSkipped || isProUser(this.chatEntitlementService.entitlement) || this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog } else { setupStrategy = await this.showDialog(); } + if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup + } + let success = undefined; try { switch (setupStrategy) { case ChatSetupStrategy.SetupWithEnterpriseProvider: - success = await this.controller.value.setupWithProvider({ setupFromDialog: true, useEnterpriseProvider: true }); + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: true }); break; case ChatSetupStrategy.SetupWithoutEnterpriseProvider: - success = await this.controller.value.setupWithProvider({ setupFromDialog: true, useEnterpriseProvider: false }); + success = await this.controller.value.setupWithProvider({ useEnterpriseProvider: false }); break; case ChatSetupStrategy.DefaultSetup: - success = await this.controller.value.setup({ setupFromDialog: true }); + success = await this.controller.value.setup(); + break; + case ChatSetupStrategy.Canceled: + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedMaybeLater', installDuration: 0, signUpErrorCode: undefined }); break; } } catch (error) { @@ -435,7 +655,7 @@ class ChatSetup { success = false; } - return success; + return { success, dialogSkipped }; } private async showDialog(): Promise { @@ -484,7 +704,7 @@ class ChatSetup { return this.context.state.registered ? localize('signUp', "Sign in to use Copilot") : localize('signUpFree', "Sign in to use Copilot for free"); } - if (this.context.state.entitlement === ChatEntitlement.Pro) { + if (isProUser(this.context.state.entitlement)) { return localize('copilotProTitle', "Start using Copilot Pro"); } @@ -492,7 +712,7 @@ class ChatSetup { } private createDialog(disposables: DisposableStore): HTMLElement { - const element = $('.chat-setup-view'); + const element = $('.chat-setup-dialog'); const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); @@ -506,7 +726,7 @@ class ChatSetup { // SKU Settings if (this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { - const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "Copilot Free and Pro may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); + const settings = copilotSettingsMessage; element.appendChild($('p.setup-settings', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); } @@ -524,7 +744,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IChatEntitlementService chatEntitlementService: ChatEntitlementService, - @IConfigurationService private readonly configurationService: IConfigurationService, @ILogService private readonly logService: ILogService, ) { super(); @@ -538,53 +757,61 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); this.registerSetupAgents(context, controller); - this.registerChatWelcome(context, controller); this.registerActions(context, requests, controller); this.registerUrlLinkHandler(); } private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy): void { - const registration = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload + const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload + const vscodeAgentDisposables = markAsSingleton(new MutableDisposable()); const updateRegistration = () => { - const disabled = context.state.hidden || !this.configurationService.getValue('chat.setupFromDialog'); - if (!disabled && !registration.value) { - const { agent: panelAgent, disposable: panelDisposable } = SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Panel, false, context, controller); - registration.value = combinedDisposable( - panelDisposable, - SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Terminal, false, context, controller).disposable, - SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Notebook, false, context, controller).disposable, - SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Editor, false, context, controller).disposable, - SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.EditingSession, false, context, controller).disposable, - SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.EditingSession, true, context, controller).disposable, - panelAgent.onUnresolvableError(() => { - // An unresolvable error from our agent registrations means that - // Copilot is unhealthy for some reason. We clear our panel - // registration to give Copilot a chance to show a custom message - // to the user from the views and stop pretending as if there was - // a functional agent. - this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.'); - panelDisposable.dispose(); - }) - ); - } else if (disabled && registration.value) { - registration.clear(); + const disabled = context.state.hidden /* via "Hide Copilot" */ || context.state.disabled /* via extension enablement */; + if (!disabled) { + + // Default Agents (always, even if installed to allow for speedy requests right on startup) + if (!defaultAgentDisposables.value) { + const disposables = defaultAgentDisposables.value = new DisposableStore(); + + // Panel Agents + const panelAgentDisposables = disposables.add(new DisposableStore()); + for (const mode of [ChatMode.Ask, ChatMode.Edit, ChatMode.Agent]) { + const { agent, disposable } = SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Panel, mode, context, controller); + panelAgentDisposables.add(disposable); + panelAgentDisposables.add(agent.onUnresolvableError(() => { + // An unresolvable error from our agent registrations means that + // Copilot is unhealthy for some reason. We clear our panel + // registration to give Copilot a chance to show a custom message + // to the user from the views and stop pretending as if there was + // a functional agent. + this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.'); + panelAgentDisposables.dispose(); + })); + } + + // Inline Agents + disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable); + disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable); + disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable); + } + + // VSCode Agent + Tool (unless installed) + if (!context.state.installed && !vscodeAgentDisposables.value) { + const disposables = vscodeAgentDisposables.value = new DisposableStore(); + + disposables.add(SetupAgent.registerVSCodeAgent(this.instantiationService, context, controller).disposable); + } + } else { + defaultAgentDisposables.clear(); + vscodeAgentDisposables.clear(); } - }; - this._register(Event.runAndSubscribe(Event.any( - context.onDidChange, - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('chat.setupFromDialog')) - ), () => updateRegistration())); - } + if (context.state.installed) { + vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list + } + }; - private registerChatWelcome(context: ChatEntitlementContext, controller: Lazy): void { - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: localize('welcomeChat', "Welcome to Copilot"), - when: ChatContextKeys.SetupViewCondition, - icon: Codicon.copilotLarge, - content: disposables => disposables.add(this.instantiationService.createInstance(ChatSetupWelcomeContent, controller.value, context)).element, - }); + this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration())); } private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy): void { @@ -603,28 +830,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr title: CHAT_SETUP_ACTION_LABEL, category: CHAT_CATEGORY, f1: true, - precondition: chatSetupTriggerContext, - menu: { - id: MenuId.ChatTitleBarMenu, - group: 'a_last', - order: 1, - when: ContextKeyExpr.and( - chatSetupTriggerContext, - ContextKeyExpr.or( - ChatContextKeys.Setup.fromDialog.negate(), // reduce noise when using the skeleton-view approach - ChatContextKeys.Setup.hidden // but enforce it if copilot is hidden - ) - ) - } + precondition: chatSetupTriggerContext }); } override async run(accessor: ServicesAccessor, mode: ChatMode): Promise { const viewsService = accessor.get(IViewsService); - const viewDescriptorService = accessor.get(IViewDescriptorService); - const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); - const statusbarService = accessor.get(IStatusbarService); const instantiationService = accessor.get(IInstantiationService); const dialogService = accessor.get(IDialogService); const commandService = accessor.get(ICommandService); @@ -638,32 +850,73 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr chatWidget?.input.setChatMode(mode); } - const setupFromDialog = configurationService.getValue('chat.setupFromDialog'); - if (!setupFromDialog) { - ensureSideBarChatViewSize(viewDescriptorService, layoutService, viewsService); - } + const setup = ChatSetup.getInstance(instantiationService, context, controller); + const { success } = await setup.run(); + if (success === false && !lifecycleService.willShutdown) { + const { confirmed } = await dialogService.confirm({ + type: Severity.Error, + message: localize('setupErrorDialog', "Copilot setup failed. Would you like to try again?"), + primaryButton: localize('retry', "Retry"), + }); - statusbarService.updateEntryVisibility('chat.statusBarEntry', true); - configurationService.updateValue('chat.commandCenter.enabled', true); - - if (setupFromDialog) { - const setup = ChatSetup.getInstance(instantiationService, context, controller); - const result = await setup.run(); - if (result === false && !lifecycleService.willShutdown) { - const { confirmed } = await dialogService.confirm({ - type: Severity.Error, - message: localize('setupErrorDialog', "Copilot setup failed. Would you like to try again?"), - primaryButton: localize('retry', "Retry"), - }); - - if (confirmed) { - commandService.executeCommand(CHAT_SETUP_ACTION_ID); - } + if (confirmed) { + commandService.executeCommand(CHAT_SETUP_ACTION_ID); } } } } + class ChatSetupTriggerWithoutDialogAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.triggerSetupWithoutDialog', + title: CHAT_SETUP_ACTION_LABEL, + precondition: chatSetupTriggerContext + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const layoutService = accessor.get(IWorkbenchLayoutService); + const instantiationService = accessor.get(IInstantiationService); + + await context.update({ hidden: false }); + + const chatWidget = await showCopilotView(viewsService, layoutService); + ChatSetup.getInstance(instantiationService, context, controller).skipDialog(); + chatWidget?.acceptInput(localize('setupCopilot', "Set up Copilot.")); + } + } + + class ChatSetupFromAccountsAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.triggerSetupFromAccounts', + title: localize2('triggerChatSetupFromAccounts', "Sign in to use Copilot..."), + menu: { + id: MenuId.AccountsContext, + group: '2_copilot', + when: ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Entitlement.signedOut + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + const telemetryService = accessor.get(ITelemetryService); + + telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'accounts' }); + + return commandService.executeCommand(CHAT_SETUP_ACTION_ID); + } + } + class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; @@ -688,9 +941,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr override async run(accessor: ServicesAccessor): Promise { const viewsDescriptorService = accessor.get(IViewDescriptorService); const layoutService = accessor.get(IWorkbenchLayoutService); - const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); - const statusbarService = accessor.get(IStatusbarService); const { confirmed } = await dialogService.confirm({ message: localize('hideChatSetupConfirm', "Are you sure you want to hide Copilot?"), @@ -712,9 +963,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar } } - - statusbarService.updateEntryVisibility('chat.statusBarEntry', false); - configurationService.updateValue('chat.commandCenter.enabled', false); } } @@ -734,9 +982,12 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr id: MenuId.ChatTitleBarMenu, group: 'a_first', order: 1, - when: ContextKeyExpr.or( - ChatContextKeys.chatQuotaExceeded, - ChatContextKeys.completionsQuotaExceeded + when: ContextKeyExpr.and( + ChatContextKeys.Entitlement.limited, + ContextKeyExpr.or( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.completionsQuotaExceeded + ) ) } }); @@ -750,7 +1001,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr openerService.open(URI.parse(defaultChat.upgradePlanUrl)); const entitlement = context.state.entitlement; - if (entitlement !== ChatEntitlement.Pro) { + if (!isProUser(entitlement)) { // If the user is not yet Pro, we listen to window focus to refresh the token // when the user has come back to the window assuming the user signed up. windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService)); @@ -762,16 +1013,54 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr windowFocusListener.clear(); const entitlements = await requests.forceResolveEntitlement(undefined); - if (entitlements?.entitlement === ChatEntitlement.Pro) { + if (entitlements?.entitlement && isProUser(entitlements?.entitlement)) { refreshTokens(commandService); } } } } + class EnableOveragesAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.manageOverages', + title: localize2('manageOverages', "Manage Copilot Overages"), + category: localize2('chat.category', 'Chat'), + f1: true, + precondition: ContextKeyExpr.or( + ChatContextKeys.Entitlement.pro, + ChatContextKeys.Entitlement.proPlus, + ), + menu: { + id: MenuId.ChatTitleBarMenu, + group: 'a_first', + order: 1, + when: ContextKeyExpr.and( + ContextKeyExpr.or( + ChatContextKeys.Entitlement.pro, + ChatContextKeys.Entitlement.proPlus, + ), + ContextKeyExpr.or( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.completionsQuotaExceeded + ) + ) + } + }); + } + + override async run(accessor: ServicesAccessor, from?: string): Promise { + const openerService = accessor.get(IOpenerService); + openerService.open(URI.parse(defaultChat.manageOveragesUrl)); + } + } + registerAction2(ChatSetupTriggerAction); + registerAction2(ChatSetupFromAccountsAction); + registerAction2(ChatSetupTriggerWithoutDialogAction); registerAction2(ChatSetupHideAction); registerAction2(UpgradePlanAction); + registerAction2(EnableOveragesAction); } private registerUrlLinkHandler(): void { @@ -801,13 +1090,11 @@ type InstallChatClassification = { installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; - setupFromDialog: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setup was triggered from the dialog or not.' }; }; type InstallChatEvent = { - installResult: 'installed' | 'alreadyInstalled' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession'; + installResult: 'installed' | 'alreadyInstalled' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession' | 'failedMaybeLater'; installDuration: number; signUpErrorCode: number | undefined; - setupFromDialog: boolean; }; enum ChatSetupStep { @@ -829,15 +1116,12 @@ class ChatSetupController extends Disposable { private readonly requests: ChatEntitlementRequests, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IProgressService private readonly progressService: IProgressService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IDialogService private readonly dialogService: IDialogService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -862,10 +1146,10 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(options?: { forceSignIn?: boolean; setupFromDialog?: boolean }): Promise { + async setup(options?: { forceSignIn?: boolean }): Promise { const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); - const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { + const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, { badge: new ProgressBadge(() => title), }); @@ -880,10 +1164,9 @@ class ChatSetupController extends Disposable { } } - private async doSetup(options: { forceSignIn?: boolean; setupFromDialog?: boolean }, watch: StopWatch): Promise { + private async doSetup(options: { forceSignIn?: boolean }, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker - let focusChatInput = false; let success = false; try { const providerId = ChatEntitlementRequests.providerId(this.configurationService); @@ -893,9 +1176,9 @@ class ChatSetupController extends Disposable { // Entitlement Unknown or `forceSignIn`: we need to sign-in user if (this.context.state.entitlement === ChatEntitlement.Unknown || options.forceSignIn) { this.setStep(ChatSetupStep.SigningIn); - const result = await this.signIn(providerId, options); + const result = await this.signIn(providerId); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog: Boolean(options.setupFromDialog) }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return false; } @@ -907,38 +1190,25 @@ class ChatSetupController extends Disposable { message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog: Boolean(options.setupFromDialog) }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return false; } - const activeElement = getActiveElement(); - // Install this.setStep(ChatSetupStep.Installing); - success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, options, watch); - - const currentActiveElement = getActiveElement(); - focusChatInput = activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body; + success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch); } finally { this.setStep(ChatSetupStep.Initial); this.context.resume(); } - if (focusChatInput && !options.setupFromDialog) { - (await showCopilotView(this.viewsService, this.layoutService))?.focusInput(); - } - return success; } - private async signIn(providerId: string, options?: { setupFromDialog?: boolean }): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { + private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; let entitlements; try { - if (!options?.setupFromDialog) { - showCopilotView(this.viewsService, this.layoutService); - } - ({ session, entitlements } = await this.requests.signIn()); } catch (e) { this.logService.error(`[chat setup] signIn: error ${e}`); @@ -953,25 +1223,22 @@ class ChatSetupController extends Disposable { }); if (confirmed) { - return this.signIn(providerId, options); + return this.signIn(providerId); } } return { session, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, options: { setupFromDialog?: boolean }, watch: StopWatch): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch): Promise { const wasInstalled = this.context.state.installed; let signUpResult: boolean | { errorCode: number } | undefined = undefined; try { - if (!options?.setupFromDialog) { - showCopilotView(this.viewsService, this.layoutService); - } if ( entitlement !== ChatEntitlement.Limited && // User is not signed up to Copilot Free - entitlement !== ChatEntitlement.Pro && // User is not signed up to Copilot Pro + !isProUser(entitlement) && // User is not signed up for a Copilot subscription entitlement !== ChatEntitlement.Unavailable // User is eligible for Copilot Free ) { if (!session) { @@ -982,7 +1249,7 @@ class ChatSetupController extends Disposable { } if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog: Boolean(options.setupFromDialog) }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return false; // unexpected } } @@ -990,30 +1257,25 @@ class ChatSetupController extends Disposable { signUpResult = await this.requests.signUpLimited(session); if (typeof signUpResult !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, setupFromDialog: Boolean(options.setupFromDialog) }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode }); } } await this.doInstall(); } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog: Boolean(options.setupFromDialog) }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined }); return false; } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasInstalled ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog: Boolean(options.setupFromDialog) }); + if (typeof signUpResult === 'boolean') { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: wasInstalled && !signUpResult ? 'alreadyInstalled' : 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + } if (wasInstalled && signUpResult === true) { refreshTokens(this.commandService); } - if (!options?.setupFromDialog) { - await Promise.race([ - timeout(5000), // helps prevent flicker with sign-in welcome view - Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 - ]); - } - return true; } @@ -1026,7 +1288,7 @@ class ChatSetupController extends Disposable { isMachineScoped: false, // do not ask to sync installEverywhere: true, // install in local and remote installPreReleaseVersion: this.productService.quality !== 'stable' - }, preferCopilotEditsView(this.viewsService) ? EditsViewId : ChatViewId); + }, ChatViewId); } catch (e) { this.logService.error(`[chat setup] install: error ${error}`); error = e; @@ -1050,7 +1312,7 @@ class ChatSetupController extends Disposable { } } - async setupWithProvider(options: { useEnterpriseProvider: boolean; setupFromDialog?: boolean }): Promise { + async setupWithProvider(options: { useEnterpriseProvider: boolean }): Promise { const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ 'id': 'copilot.setup', @@ -1169,129 +1431,6 @@ class ChatSetupController extends Disposable { //#endregion -//#region Setup View Welcome - -class ChatSetupWelcomeContent extends Disposable { - - readonly element = $('.chat-setup-view'); - - constructor( - private readonly controller: ChatSetupController, - private readonly context: ChatEntitlementContext, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - ) { - super(); - - this.create(); - } - - private create(): void { - const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); - - // Header - { - const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", this.context.state.installed ? `command:${defaultChat.walkthroughCommand}` : defaultChat.documentationUrl); - this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element)); - - this.element.appendChild( - $('div.chat-features-container', undefined, - $('div', undefined, - $('div.chat-feature-container', undefined, - renderIcon(Codicon.code), - $('span', undefined, localize('featureChat', "Code faster with Completions")) - ), - $('div.chat-feature-container', undefined, - renderIcon(Codicon.editSession), - $('span', undefined, localize('featureEdits', "Build features with Copilot Edits")) - ), - $('div.chat-feature-container', undefined, - renderIcon(Codicon.commentDiscussion), - $('span', undefined, localize('featureExplore', "Explore your codebase with Chat")) - ) - ) - ) - ); - } - - // Limited SKU - const free = localize({ key: 'free', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}).", defaultChat.skusDocumentationUrl); - const freeContainer = this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element)); - - // Setup Button - const buttonContainer = this.element.appendChild($('p')); - buttonContainer.classList.add('button-container'); - const button = this._register(new ButtonWithDropdown(buttonContainer, { - actions: [ - toAction({ id: 'chatSetup.setupWithProvider', label: localize('setupWithProvider', "Sign in with a {0} Account", defaultChat.providerName), run: () => this.controller.setupWithProvider({ useEnterpriseProvider: false }) }), - toAction({ id: 'chatSetup.setupWithEnterpriseProvider', label: localize('setupWithEnterpriseProvider', "Sign in with a {0} Account", defaultChat.enterpriseProviderName), run: () => this.controller.setupWithProvider({ useEnterpriseProvider: true }) }) - ], - addPrimaryActionToDropdown: false, - contextMenuProvider: this.contextMenuService, - supportIcons: true, - ...defaultButtonStyles - })); - this._register(button.onDidClick(() => this.controller.setup())); - - // Terms - const terms = localize({ key: 'terms', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to the [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl); - this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element)); - - // SKU Settings - const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "Copilot Free and Pro may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); - const settingsContainer = this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); - - // Update based on model state - this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(freeContainer, settingsContainer, button))); - } - - private update(freeContainer: HTMLElement, settingsContainer: HTMLElement, button: ButtonWithDropdown): void { - const showSettings = this.telemetryService.telemetryLevel !== TelemetryLevel.NONE; - let showFree: boolean; - let buttonLabel: string; - - switch (this.context.state.entitlement) { - case ChatEntitlement.Unknown: - showFree = true; - buttonLabel = this.context.state.registered ? localize('signUp', "Sign in to use Copilot") : localize('signUpFree', "Sign in to use Copilot for free"); - break; - case ChatEntitlement.Unresolved: - showFree = true; - buttonLabel = this.context.state.registered ? localize('startUp', "Use Copilot") : localize('startUpLimited', "Use Copilot for free"); - break; - case ChatEntitlement.Available: - case ChatEntitlement.Limited: - showFree = true; - buttonLabel = localize('startUpLimited', "Use Copilot for free"); - break; - case ChatEntitlement.Pro: - case ChatEntitlement.Unavailable: - showFree = false; - buttonLabel = localize('startUp', "Use Copilot"); - break; - } - - switch (this.controller.step) { - case ChatSetupStep.SigningIn: - buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName); - break; - case ChatSetupStep.Installing: - buttonLabel = localize('setupChatInstalling', "$(loading~spin) Getting Copilot Ready..."); - break; - } - - setVisibility(showFree, freeContainer); - setVisibility(showSettings, settingsContainer); - - button.label = buttonLabel; - button.enabled = this.controller.step === ChatSetupStep.Initial; - } -} - -//#endregion - function refreshTokens(commandService: ICommandService): void { // ugly, but we need to signal to the extension that entitlements changed commandService.executeCommand(defaultChat.completionsRefreshTokenCommand); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/code/src/vs/workbench/contrib/chat/browser/chatStatus.ts index d4d6718a816..32ec67c64d4 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -11,7 +11,7 @@ import { localize } from '../../../../nls.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType } from '../../../../base/browser/dom.js'; -import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService } from '../common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService, IQuotaSnapshot } from '../common/chatEntitlementService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; @@ -19,7 +19,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js'; import { Color } from '../../../../base/common/color.js'; import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -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'; @@ -38,6 +38,10 @@ import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService 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, @@ -93,7 +97,9 @@ 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 ?? '', + manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '', }; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { @@ -115,20 +121,17 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu ) { super(); - this.create(); + this.update(); this.registerListeners(); } - private async create(): Promise { - const hidden = this.chatEntitlementService.sentiment === ChatSentiment.Disabled; - - if (!hidden) { - this.entry ||= this.statusbarService.addEntry(this.getEntryProps(), 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); - - // TODO@bpasero: remove this eventually - const completionsStatusId = `${defaultChat.extensionId}.status`; - this.statusbarService.updateEntryVisibility(completionsStatusId, false); - this.statusbarService.overrideEntry(completionsStatusId, { name: localize('codeCompletionsStatus', "Copilot Code Completions"), text: localize('codeCompletionsStatusText', "$(copilot) Completions") }); + private update(): void { + if (this.chatEntitlementService.sentiment !== ChatSentiment.Disabled) { + if (!this.entry) { + this.entry = this.statusbarService.addEntry(this.getEntryProps(), 'chat.statusBarEntry', StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); + } else { + this.entry.update(this.getEntryProps()); + } } else { this.entry?.dispose(); this.entry = undefined; @@ -136,21 +139,21 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } private registerListeners(): void { - 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.chatEntitlementService.onDidChangeQuotaExceeded(() => this.update())); + this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.update())); + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.update())); this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) { - this.entry?.update(this.getEntryProps()); + this.update(); } })); } private onDidActiveEditorChange(): void { - this.entry?.update(this.getEntryProps()); + this.update(); this.activeCodeEditorListener.clear(); @@ -158,7 +161,7 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); if (activeCodeEditor) { this.activeCodeEditorListener.value = activeCodeEditor.onDidChangeModelLanguage(() => { - this.entry?.update(this.getEntryProps()); + this.update(); }); } } @@ -169,7 +172,8 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu let kind: StatusbarEntryKind | undefined; if (!isNewUser(this.chatEntitlementService)) { - const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; + const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; + const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; // Signed out if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { @@ -180,15 +184,15 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu kind = 'prominent'; } - // Quota Exceeded - else if (chatQuotaExceeded || completionsQuotaExceeded) { + // Free Quota Exceeded + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited && (chatQuotaExceeded || completionsQuotaExceeded)) { let quotaWarning: string; if (chatQuotaExceeded && !completionsQuotaExceeded) { - quotaWarning = localize('chatQuotaExceededStatus', "Chat limit reached"); + quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached"); } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - quotaWarning = localize('completionsQuotaExceededStatus', "Completions limit reached"); + quotaWarning = localize('completionsQuotaExceededStatus', "Completions quota reached"); } else { - quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Limit reached"); + quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached"); } text = `$(copilot-warning) ${quotaWarning}`; @@ -198,7 +202,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"); } } @@ -230,9 +234,10 @@ function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { function canUseCopilot(chatEntitlementService: IChatEntitlementService): boolean { const newUser = isNewUser(chatEntitlementService); const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; - const allQuotaReached = chatEntitlementService.quotas.chatQuotaExceeded && chatEntitlementService.quotas.completionsQuotaExceeded; + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; + const allFreeQuotaReached = limited && chatEntitlementService.quotas.chat?.percentRemaining === 0 && chatEntitlementService.quotas.completions?.percentRemaining === 0; - return !newUser && !signedOut && !allQuotaReached; + return !newUser && !signedOut && !allFreeQuotaReached; } function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { @@ -253,6 +258,19 @@ interface ISettingsAccessor { writeSetting: (value: boolean) => Promise; } +type ChatSettingChangedClassification = { + owner: 'bpasero'; + comment: 'Provides insight into chat settings changed from the chat status entry.'; + settingIdentifier: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the setting that changed.' }; + settingMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The optional editor language for which the setting changed.' }; + settingEnablement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting got enabled or disabled.' }; +}; +type ChatSettingChangedEvent = { + settingIdentifier: string; + settingMode?: string; + settingEnablement: 'enabled' | 'disabled'; +}; + class ChatStatusDashboard extends Disposable { private readonly element = $('div.chat-status-bar-entry-tooltip'); @@ -282,34 +300,42 @@ 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) { + this.renderHeader(this.element, disposables, label ?? '', action); } needsSeparator = true; }; // Quota Indicator - if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate, chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; - - addSeparator(localize('usageTitle', "Copilot Free Plan Usage")); + const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate } = this.chatEntitlementService.quotas; + if (chatQuota || completionsQuota || premiumChatQuota) { + + addSeparator(localize('usageTitle', "Copilot Usage"), toAction({ + id: 'workbench.action.manageCopilot', + 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")); + const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; + const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined; + const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, disposables, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined; - this.element.appendChild($('div.description', undefined, localize('limitQuota', "Limits will reset on {0}.", this.dateFormatter.value.format(quotaResetDate)))); + if (resetDate) { + this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance resets {0}.", this.dateFormatter.value.format(new Date(resetDate))))); + } - if (chatQuotaExceeded || completionsQuotaExceeded) { - const upgradePlanButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); - upgradePlanButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); - disposables.add(upgradePlanButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan'))); + if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) { + const upgradeProButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); + upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); + disposables.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan'))); } (async () => { @@ -318,31 +344,38 @@ class ChatStatusDashboard extends Disposable { return; } - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = this.chatEntitlementService.quotas; - - chatQuotaIndicator(chatTotal, chatRemaining); - completionsQuotaIndicator(completionsTotal, completionsRemaining); + const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas; + if (completionsQuota) { + completionsQuotaIndicator?.(completionsQuota); + } + if (chatQuota) { + chatQuotaIndicator?.(chatQuota); + } + if (premiumChatQuota) { + premiumChatQuotaIndicator?.(premiumChatQuota); + } })(); } // 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); } })); } @@ -350,7 +383,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); } @@ -360,7 +399,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."))); @@ -373,24 +412,41 @@ class ChatStatusDashboard extends Disposable { return this.element; } + private renderHeader(container: HTMLElement, disposables: DisposableStore, label: string, action?: IAction): void { + const header = container.appendChild($('div.header', undefined, label ?? '')); + + if (action) { + const toolbar = disposables.add(new ActionBar(header, { hoverDelegate: nativeHoverDelegate })); + toolbar.push([action], { icon: true, label: false }); + } + } + 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)); + const headerLabel = typeof item.label === 'string' ? item.label : item.label.label; + const headerLink = typeof item.label === 'string' ? undefined : item.label.link; + this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({ + id: 'workbench.action.openChatStatusItemLink', + label: localize('learnMore', "Learn More"), + tooltip: localize('learnMore', "Learn More"), + class: ThemeIcon.asClassName(Codicon.linkExternal), + run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))), + }) : undefined); - 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 { @@ -415,42 +471,72 @@ class ChatStatusDashboard extends Disposable { this.hoverService.hideHover(true); } - private createQuotaIndicator(container: HTMLElement, total: number | undefined, remaining: number | undefined, label: string): (total: number | undefined, remaining: number | undefined) => void { - const quotaText = $('span'); + private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { + const quotaValue = $('span.quota-value'); const quotaBit = $('div.quota-bit'); + const overageLabel = $('span.overage-label'); const quotaIndicator = container.appendChild($('div.quota-indicator', undefined, $('div.quota-label', undefined, $('span', undefined, label), - quotaText + quotaValue ), $('div.quota-bar', undefined, quotaBit + ), + $('div.description', undefined, + overageLabel ) )); - const update = (total: number | undefined, remaining: number | undefined) => { + if (supportsOverage && (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.ProPlus)) { + const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true })); + manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests"); + disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); + } + + const update = (quota: IQuotaSnapshot) => { quotaIndicator.classList.remove('error'); quotaIndicator.classList.remove('warning'); - if (typeof total === 'number' && typeof remaining === 'number') { - let usedPercentage = Math.round(((total - remaining) / total) * 100); - if (total !== remaining && usedPercentage === 0) { - usedPercentage = 1; // indicate minimal usage as 1% - } + let usedPercentage: number; + if (quota.unlimited) { + usedPercentage = 0; + } else { + usedPercentage = Math.max(0, 100 - quota.percentRemaining); + } + // Use intl number format to format the presented numbers + const quotaPercentageFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 0 }); + const overageFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0 }); + + if (quota.unlimited) { + quotaValue.textContent = localize('quotaUnlimited', "Included"); + } else if (quota.overageCount) { + quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", overageFormatter.format(quota.overageCount)); + } else { + quotaValue.textContent = localize('quotaDisplay', "{0}%", quotaPercentageFormatter.format(usedPercentage)); + } - quotaText.textContent = localize('quotaDisplay', "{0}%", usedPercentage); - quotaBit.style.width = `${usedPercentage}%`; + quotaBit.style.width = `${usedPercentage}%`; - if (usedPercentage >= 90) { - quotaIndicator.classList.add('error'); - } else if (usedPercentage >= 75) { - quotaIndicator.classList.add('warning'); + if (usedPercentage >= 90) { + quotaIndicator.classList.add('error'); + } else if (usedPercentage >= 75) { + quotaIndicator.classList.add('warning'); + } + + if (supportsOverage) { + if (quota.overageEnabled) { + overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled."); + } else { + overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); } + } else { + overageLabel.textContent = ''; } }; - update(total, remaining); + update(quota); return update; } @@ -473,13 +559,13 @@ class ChatStatusDashboard extends Disposable { // --- Next Edit Suggestions { const setting = append(settings, $('div.setting')); - this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next Edit Suggestions"), modeId, this.getCompletionsSettingAccessor(modeId), disposables); + this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next Edit Suggestions"), this.getCompletionsSettingAccessor(modeId), disposables); } return settings; } - private createSetting(container: HTMLElement, settingId: string, label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox { + private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox { const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), defaultCheckboxStyles)); container.appendChild(checkbox.domNode); @@ -502,7 +588,7 @@ class ChatStatusDashboard extends Disposable { })); disposables.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(settingId)) { + if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) { checkbox.checked = Boolean(accessor.readSetting()); } })); @@ -510,13 +596,14 @@ class ChatStatusDashboard extends Disposable { if (!canUseCopilot(this.chatEntitlementService)) { container.classList.add('disabled'); checkbox.disable(); + checkbox.checked = false; } return checkbox; } private createCodeCompletionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void { - this.createSetting(container, defaultChat.completionsEnablementSetting, label, this.getCompletionsSettingAccessor(modeId), disposables); + this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables); } private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor { @@ -525,6 +612,12 @@ class ChatStatusDashboard extends Disposable { return { readSetting: () => isCompletionsEnabled(this.configurationService, modeId), writeSetting: (value: boolean) => { + this.telemetryService.publicLog2('chatStatus.settingChanged', { + settingIdentifier: settingId, + settingMode: modeId, + settingEnablement: value ? 'enabled' : 'disabled' + }); + let result = this.configurationService.getValue>(settingId); if (!isObject(result)) { result = Object.create(null); @@ -535,14 +628,21 @@ class ChatStatusDashboard extends Disposable { }; } - private createNextEditSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void { + private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void { const nesSettingId = defaultChat.nextEditSuggestionsSetting; const completionsSettingId = defaultChat.completionsEnablementSetting; const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - const checkbox = this.createSetting(container, nesSettingId, label, { - readSetting: () => this.textResourceConfigurationService.getValue(resource, nesSettingId), - writeSetting: (value: boolean) => this.textResourceConfigurationService.updateValue(resource, nesSettingId, value) + const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, { + readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue(resource, nesSettingId), + writeSetting: (value: boolean) => { + this.telemetryService.publicLog2('chatStatus.settingChanged', { + settingIdentifier: nesSettingId, + settingEnablement: value ? 'enabled' : 'disabled' + }); + + return this.textResourceConfigurationService.updateValue(resource, nesSettingId, value); + } }, disposables); // enablement of NES depends on completions setting diff --git a/code/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts b/code/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts index 3f009e58eb0..91697c5cf83 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts @@ -28,7 +28,7 @@ export interface IChatStatusItemChangeEvent { export type ChatStatusEntry = { id: string; - label: string; + label: string | { label: string; link: string }; description: string; detail: string | undefined; }; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts index f822997f5ae..573f54f0aad 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -3,16 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../base/common/arrays.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IChatRequestVariableData, IChatRequestVariableEntry } from '../common/chatModel.js'; -import { ChatRequestDynamicVariablePart, ChatRequestToolPart, IParsedChatRequest } from '../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js'; -import { ChatAgentLocation, ChatConfiguration } from '../common/constants.js'; -import { IChatWidgetService, showChatView, showEditsView } from './chat.js'; +import { IToolData } from '../common/languageModelToolsService.js'; +import { IChatWidgetService } from './chat.js'; import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js'; export class ChatVariablesService implements IChatVariablesService { @@ -20,37 +13,7 @@ export class ChatVariablesService implements IChatVariablesService { constructor( @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IViewsService private readonly viewsService: IViewsService, - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - } - - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData { - let resolvedVariables: IChatRequestVariableEntry[] = []; - - prompt.parts - .forEach((part, i) => { - if (part instanceof ChatRequestDynamicVariablePart || part instanceof ChatRequestToolPart) { - resolvedVariables[i] = part.toVariableEntry(); - } - }); - - // Make array not sparse - resolvedVariables = coalesce(resolvedVariables); - - // "reverse", high index first so that replacement is simple - resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); - - if (attachedContextVariables) { - // attachments not in the prompt - resolvedVariables.push(...attachedContextVariables); - } - - - return { - variables: resolvedVariables, - }; - } + ) { } getDynamicVariables(sessionId: string): ReadonlyArray { // This is slightly wrong... the parser pulls dynamic references from the input widget, but there is no guarantee that message came from the input here. @@ -70,30 +33,12 @@ export class ChatVariablesService implements IChatVariablesService { return model.variables; } - async attachContext(name: string, value: string | URI | Location, location: ChatAgentLocation) { - if (location !== ChatAgentLocation.Panel && location !== ChatAgentLocation.EditingSession) { - return; - } - - const unifiedViewEnabled = !!this.configurationService.getValue(ChatConfiguration.UnifiedChatView); - const widget = location === ChatAgentLocation.EditingSession && !unifiedViewEnabled - ? await showEditsView(this.viewsService) - : (this.chatWidgetService.lastFocusedWidget ?? await showChatView(this.viewsService)); - if (!widget || !widget.viewModel) { - return; - } - - const key = name.toLowerCase(); - if (key === 'file' && typeof value !== 'string') { - const uri = URI.isUri(value) ? value : value.uri; - const range = 'range' in value ? value.range : undefined; - await widget.attachmentModel.addFile(uri, range); - return; - } - - if (key === 'folder' && URI.isUri(value)) { - widget.attachmentModel.addFolder(value); - return; + getSelectedTools(sessionId: string): ReadonlyArray { + const widget = this.chatWidgetService.getWidgetBySessionId(sessionId); + if (!widget) { + return []; } + return widget.input.selectedToolsModel.tools.get(); } + } diff --git a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 9d7c8445f65..cf16001e255 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -26,7 +26,6 @@ import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { IChatViewTitleActionContext } from '../common/chatActions.js'; import { IChatAgentService } from '../common/chatAgents.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatModelInitState, IChatModel } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IChatService } from '../common/chatService.js'; @@ -41,7 +40,6 @@ interface IViewPaneState extends IChatViewState { export const CHAT_SIDEBAR_OLD_VIEW_PANEL_ID = 'workbench.panel.chatSidebar'; export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chat'; -export const CHAT_EDITING_SIDEBAR_PANEL_ID = 'workbench.panel.chatEditing'; export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } @@ -55,7 +53,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private _restoringSession: Promise | undefined; constructor( - private readonly chatOptions: { location: ChatAgentLocation.Panel | ChatAgentLocation.EditingSession }, + private readonly chatOptions: { location: ChatAgentLocation.Panel }, options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -75,10 +73,10 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); // View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento. - this.memento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID + (this.chatOptions.location === ChatAgentLocation.EditingSession ? `-edits` : ''), this.storageService); + this.memento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID, this.storageService); this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState; - if (this.chatService.unifiedViewEnabled && this.chatOptions.location === ChatAgentLocation.Panel && !this.viewState.hasMigratedCurrentSession) { + if (this.chatOptions.location === ChatAgentLocation.Panel && !this.viewState.hasMigratedCurrentSession) { const editsMemento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID + `-edits`, this.storageService); const lastEditsState = editsMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState; if (lastEditsState.sessionId) { @@ -102,6 +100,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const info = this.getTransferredOrPersistedSessionInfo(); this._restoringSession = (info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : Promise.resolve(undefined)).then(async model => { + if (!this._widget) { + // renderBody has not been called yet + return; + } + // The widget may be hidden at this point, because welcome views were allowed. Use setVisible to // avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome` // so it should fire onDidChangeViewWelcomeState. @@ -125,12 +128,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._onDidChangeViewWelcomeState.fire(); })); - - this._register(this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(ChatContextKeys.SetupViewKeys)) { - this._onDidChangeViewWelcomeState.fire(); - } - })); } override getActionsContext(): IChatViewTitleActionContext | undefined { @@ -162,11 +159,10 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } override shouldShowWelcome(): boolean { - const showSetup = this.contextKeyService.contextMatchesRules(ChatContextKeys.SetupViewCondition); const noPersistedSessions = !this.chatService.hasSessions(); const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(this.chatOptions.location)); - const shouldShow = !hasCoreAgent && (this.didUnregisterProvider || !this._widget?.viewModel && noPersistedSessions || this.defaultParticipantRegistrationFailed || showSetup); - this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} didUnregister=${this.didUnregisterProvider} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions} || defaultParticipantRegistrationFailed=${this.defaultParticipantRegistrationFailed} || showSetup=${showSetup}`); + const shouldShow = !hasCoreAgent && (this.didUnregisterProvider || !this._widget?.viewModel && noPersistedSessions || this.defaultParticipantRegistrationFailed); + this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} didUnregister=${this.didUnregisterProvider} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions} || defaultParticipantRegistrationFailed=${this.defaultParticipantRegistrationFailed}`); return !!shouldShow; } @@ -202,18 +198,17 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { autoScroll: mode => mode !== ChatMode.Ask, renderFollowups: this.chatOptions.location === ChatAgentLocation.Panel, supportsFileReferences: true, - supportsAdditionalParticipants: this.chatOptions.location === ChatAgentLocation.Panel, rendererOptions: { renderTextEditsAsSummary: (uri) => { - return this.chatService.isEditingLocation(this.chatOptions.location); + return true; }, - referencesExpandedWhenEmptyResponse: !this.chatService.isEditingLocation(this.chatOptions.location), + referencesExpandedWhenEmptyResponse: false, progressMessageAtBottomOfResponse: mode => mode !== ChatMode.Ask, }, editorOverflowWidgetsDomNode: editorOverflowNode, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel || this.chatService.isEditingLocation(this.chatOptions.location), - enableWorkingSet: this.chatService.isEditingLocation(this.chatOptions.location) ? 'explicit' : undefined, - supportsChangingModes: this.chatService.isEditingLocation(this.chatOptions.location), + enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, + enableWorkingSet: 'explicit', + supportsChangingModes: true, }, { listForeground: SIDE_BAR_FOREGROUND, diff --git a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts index efe0571931a..1d8bc8ce7c5 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -6,24 +6,28 @@ import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; +import { pick } from '../../../../base/common/arrays.js'; +import { assert } from '../../../../base/common/assert.js'; import { disposableTimeout, timeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { memoize } from '../../../../base/common/decorators.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../base/common/iterator.js'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { extUri, isEqual } from '../../../../base/common/resources.js'; +import { autorun, autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { basename, extUri, isEqual } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { isLocation, Location } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -33,27 +37,30 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; 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 { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent } 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 { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; import { IChatSlashCommandService } from '../common/chatSlashCommands.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { IChatInputState } from '../common/chatWidgetHistoryService.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatMode } from '../common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../common/constants.js'; +import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; +import { IPromptsService } from '../common/promptSyntax/service/types.js'; +import { IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; +import { isPromptFileChatVariable, toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; import { ChatInputPart, IChatInputStyles } from './chatInputPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; @@ -97,8 +104,6 @@ export function isQuickChat(widget: IChatWidget): boolean { return 'viewContext' in widget && 'isQuickChat' in widget.viewContext && Boolean(widget.viewContext.isQuickChat); } -const PersistWelcomeMessageContentKey = 'chat.welcomeMessageContent'; - export class ChatWidget extends Disposable implements IChatWidget { public static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = []; @@ -150,8 +155,11 @@ export class ChatWidget extends Disposable implements IChatWidget { private listContainer!: HTMLElement; private container!: HTMLElement; + get domNode() { + return this.container; + } + private welcomeMessageContainer!: HTMLElement; - private persistedWelcomeMessage: IChatWelcomeMessageContent | undefined; private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); private bodyDimension: dom.Dimension | undefined; @@ -175,6 +183,9 @@ export class ChatWidget extends Disposable implements IChatWidget { */ private scrollLock = true; + private _isReady = false; + private _onDidBecomeReady = this._register(new Emitter()); + private readonly viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; private set viewModel(viewModel: ChatViewModel | undefined) { @@ -187,6 +198,20 @@ export class ChatWidget extends Disposable implements IChatWidget { this._viewModel = viewModel; if (viewModel) { this.viewModelDisposables.add(viewModel); + this.logService.debug('ChatWidget#setViewModel: have viewModel'); + + if (viewModel.model.editingSessionObs) { + this.logService.debug('ChatWidget#setViewModel: waiting for editing session'); + viewModel.model.editingSessionObs?.promise.then(() => { + this._isReady = true; + this._onDidBecomeReady.fire(); + }); + } else { + this._isReady = true; + this._onDidBecomeReady.fire(); + } + } else { + this.logService.debug('ChatWidget#setViewModel: no viewModel'); } this._onDidChangeViewModel.fire(); @@ -206,6 +231,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentMode }); + this._onDidChangeParsedInput.fire(); } return this.parsedChatRequest; @@ -222,9 +248,8 @@ export class ChatWidget extends Disposable implements IChatWidget { readonly viewContext: IChatWidgetViewContext; - @memoize - get isUnifiedPanelWidget(): boolean { - return this._location.location === ChatAgentLocation.Panel && !!this.viewOptions.supportsChangingModes && this.configurationService.getValue(ChatConfiguration.UnifiedChatView); + get supportsChangingModes(): boolean { + return !!this.viewOptions.supportsChangingModes; } constructor( @@ -245,8 +270,10 @@ export class ChatWidget extends Disposable implements IChatWidget { @IThemeService private readonly themeService: IThemeService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatEditingService chatEditingService: IChatEditingService, - @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IPromptsService private readonly promptsService: IPromptsService, + @ICommandService private readonly commandService: ICommandService, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, ) { super(); @@ -263,8 +290,6 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true); ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location); ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this)); - ChatContextKeys.inUnifiedChat.bindTo(contextKeyService) - .set(this._location.location === ChatAgentLocation.Panel && !!this.viewOptions.supportsChangingModes && this.configurationService.getValue(ChatConfiguration.UnifiedChatView)); this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService); this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService); this.isRequestPaused = ChatContextKeys.isRequestPaused.bindTo(contextKeyService); @@ -276,13 +301,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 +359,13 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } + const entries = session.entries.read(r); + for (const entry of entries) { + entry.state.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(); @@ -411,11 +438,6 @@ export class ChatWidget extends Disposable implements IChatWidget { return null; })); - const loadedWelcomeContent = storageService.getObject(`${PersistWelcomeMessageContentKey}.${this.location}`, StorageScope.APPLICATION); - if (isChatWelcomeMessageContent(loadedWelcomeContent)) { - this.persistedWelcomeMessage = loadedWelcomeContent; - } - this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext())); } @@ -454,6 +476,22 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.attachmentModel; } + async waitForReady(): Promise { + if (this._isReady) { + this.logService.debug('ChatWidget#waitForReady: already ready'); + return; + } + + this.logService.debug('ChatWidget#waitForReady: waiting for ready'); + await Event.toPromise(this._onDidBecomeReady.event); + + if (this.viewModel) { + this.logService.debug('ChatWidget#waitForReady: ready'); + } else { + this.logService.debug('ChatWidget#waitForReady: no viewModel'); + } + } + render(parent: HTMLElement): void { const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined; this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground)); @@ -507,6 +545,32 @@ export class ChatWidget extends Disposable implements IChatWidget { }).filter(isDefined); this._register((this.chatWidgetService as ChatWidgetService).register(this)); + + const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput); + this._register(autorun(r => { + const input = parsedInput.read(r); + + const newPromptAttachments = new Map(); + const oldPromptAttachments = new Set(); + + // get all attachments, know those that are prompt-referenced + for (const attachment of this.attachmentModel.attachments) { + if (attachment.range) { + oldPromptAttachments.add(attachment.id); + } + } + + // update/insert prompt-referenced attachments + for (const part of input.parts) { + if (part instanceof ChatRequestToolPart || part instanceof ChatRequestDynamicVariablePart) { + const entry = part.toVariableEntry(); + newPromptAttachments.set(entry.id, entry); + oldPromptAttachments.delete(entry.id); + } + } + + this.attachmentModel.updateContent(oldPromptAttachments, newPromptAttachments.values()); + })); } private scrollToEnd() { @@ -562,6 +626,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } clear(): void { + this.logService.debug('ChatWidget#clear'); + this._isReady = false; if (this._dynamicMessageLayoutData) { this._dynamicMessageLayoutData.enabled = true; } @@ -626,16 +692,17 @@ export class ChatWidget extends Disposable implements IChatWidget { } const numItems = this.viewModel?.getItems().length ?? 0; - const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode); - const welcomeContent = defaultAgent?.metadata.welcomeMessageContent ?? this.persistedWelcomeMessage; - if (welcomeContent && !numItems && (this.welcomeMessageContainer.children.length === 0 || this.chatService.unifiedViewEnabled)) { + if (!numItems) { + const welcomeContent = this.getWelcomeViewContent(); dom.clearNode(this.welcomeMessageContainer); - const tips = this.viewOptions.supportsAdditionalParticipants + const tips = this.input.currentMode === ChatMode.Ask ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode); + const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; this.welcomePart.value = this.instantiationService.createInstance( ChatViewWelcomePart, - { ...welcomeContent, tips, }, + { ...welcomeContent, tips, additionalMessage }, { location: this.location, isWidgetAgentWelcomeViewContent: this.input?.currentMode === ChatMode.Agent @@ -650,6 +717,29 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + private getWelcomeViewContent(): IChatWelcomeMessageContent { + const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); + if (this.input.currentMode === ChatMode.Ask) { + return { + title: localize('chatDescription', "Ask Copilot"), + message: new MarkdownString(baseMessage), + icon: Codicon.copilotLarge + }; + } else if (this.input.currentMode === ChatMode.Edit) { + return { + title: localize('editsTitle', "Edit with Copilot"), + message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.") + `\n\n${baseMessage}`), + icon: Codicon.copilotLarge + }; + } else { + return { + title: localize('editsTitle', "Edit with Copilot"), + message: new MarkdownString(localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent') + `\n\n${baseMessage}`), + icon: Codicon.copilotLarge + }; + } + } + private async renderChatEditingSessionState() { if (!this.inputPart) { return; @@ -731,7 +821,6 @@ export class ChatWidget extends Disposable implements IChatWidget { attempt: request.attempt + 1, location: this.location, userSelectedModelId: this.input.currentLanguageModel, - hasInstructionAttachments: this.input.hasInstructionAttachments, mode: this.input.currentMode, }; this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e)); @@ -779,7 +868,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeTreeContentHeight(); })); this._register(this.renderer.onDidChangeItemHeight(e => { - this.tree.updateElementHeight(e.element, e.height); + if (this.tree.hasElement(e.element)) { + this.tree.updateElementHeight(e.element, e.height); + } })); this._register(this.tree.onDidFocus(() => { this._onDidFocus.fire(); @@ -857,6 +948,7 @@ export class ChatWidget extends Disposable implements IChatWidget { enableImplicitContext: this.viewOptions.enableImplicitContext, renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit', supportsChangingModes: this.viewOptions.supportsChangingModes, + dndContainer: this.viewOptions.dndContainer, widgetViewKindTag: this.getWidgetViewKindTag() }, this.styles, @@ -922,7 +1014,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); - this._register(this.inputPart.attachmentModel.onDidChangeContext(() => { + this._register(this.inputPart.attachmentModel.onDidChange(() => { if (this._editingSession) { // TODO still needed? Do this inside input part and fire onDidChangeHeight? this.renderChatEditingSessionState(); @@ -943,6 +1035,15 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderWelcomeViewContentIfNeeded(); this.refreshParsedInput(); })); + this._register(autorun(r => { + const enabledTools = new Set(this.input.selectedToolsModel.tools.read(r).map(t => t.id)); + const disabledTools = this.inputPart.attachmentModel.attachments + .filter(a => a.kind === 'tool' && !enabledTools.has(a.id)) + .map(a => a.id); + + this.inputPart.attachmentModel.updateContent(disabledTools, Iterable.empty()); + this.refreshParsedInput(); + })); } private onDidStyleChange(): void { @@ -969,7 +1070,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.setAttribute('data-session-id', model.sessionId); this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); - this.viewModelDisposables.add(Event.accumulate(this.viewModel.onDidChange, 0)(events => { + this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, 0), (events => { if (!this.viewModel) { return; } @@ -979,14 +1080,14 @@ export class ChatWidget extends Disposable implements IChatWidget { this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); this.onDidChangeItems(); - if (events.some(e => e?.kind === 'addRequest') && this.visible) { + if (events?.some(e => e?.kind === 'addRequest') && this.visible) { this.scrollToEnd(); } if (this._editingSession) { this.renderChatEditingSessionState(); } - })); + }))); this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { // Ensure that view state is saved here, because we will load it again when a new model is assigned this.inputPart.saveState(); @@ -1093,6 +1194,29 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } + private async _handlePromptSlashCommand(input: string, attachedContext: IChatRequestVariableEntry[]): Promise { + + const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart); + if (!agentSlashPromptPart) { + return input; + } + // remove the slash command from the input + input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); + + const promptPath = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand); + if (!promptPath) { + return input; + } + + if (!attachedContext.some(variable => isPromptFileChatVariable(variable) && isEqual(toUri(variable), promptPath.uri))) { + // not yet attached, so attach it + const variable = toChatVariable({ uri: promptPath.uri, isPromptFile: true }, true); + attachedContext.push(variable); + } + + return input; + } + private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise { if (this.viewModel?.requestInProgress && this.viewModel.requestPausibility !== ChatPauseState.Paused) { return; @@ -1104,23 +1228,20 @@ export class ChatWidget extends Disposable implements IChatWidget { const editorValue = this.getInput(); const requestId = this.chatAccessibilityService.acceptRequest(); - const input = !query ? editorValue : query.query; + let input = !query ? editorValue : query.query; const isUserQuery = !query; + let attachedContext = await this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); + const { promptInstructions } = this.inputPart.attachmentModel; const instructionsEnabled = promptInstructions.featureEnabled; if (instructionsEnabled) { - // instruction files may have nested child references to other prompt - // files that are resolved asynchronously, hence we need to wait for - // the entire prompt instruction tree to be processed - const instructionsStarted = performance.now(); - await promptInstructions.allSettled(); - // allow-any-unicode-next-line - this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); + input = await this._handlePromptSlashCommand(input, attachedContext); + await this.autoAttachInstructions(attachedContext); + input = await this.setupChatModeAndTools(input, attachedContext); } - let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); - if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode !== ChatMode.Ask) { + if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext; @@ -1128,7 +1249,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const previousRequests = this.viewModel.model.getRequests(); for (const request of previousRequests) { for (const variable of request.variableData.variables) { - if (URI.isUri(variable.value) && variable.isFile) { + if (URI.isUri(variable.value) && variable.kind === 'file') { const uri = variable.value; if (!uniqueWorkingSetEntries.has(uri)) { editingSessionAttachedContext.push(variable); @@ -1155,6 +1276,18 @@ export class ChatWidget extends Disposable implements IChatWidget { this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); this.input.validateCurrentMode(); + + let userSelectedTools: string[] | undefined; + let userSelectedTools2: Record | undefined; + if (this.input.currentMode === ChatMode.Agent) { + userSelectedTools = Array.from(this.inputPart.selectedToolsModel.asEnablementMap().entries()).map(([tool]) => tool.id); + + userSelectedTools2 = {}; + for (const [tool, enablement] of this.inputPart.selectedToolsModel.asEnablementMap()) { + userSelectedTools2[tool.id] = enablement; + } + } + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { mode: this.inputPart.currentMode, userSelectedModelId: this.inputPart.currentLanguageModel, @@ -1163,8 +1296,8 @@ export class ChatWidget extends Disposable implements IChatWidget { parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.inputPart.currentMode }, attachedContext, noCommandDetection: options?.noCommandDetection, - hasInstructionAttachments: this.inputPart.hasInstructionAttachments, - userSelectedTools: this.input.currentMode === ChatMode.Agent ? this.inputPart.selectedToolsModel.tools.get().map(tool => tool.id) : undefined + userSelectedTools, + userSelectedTools2, }); if (result) { @@ -1224,30 +1357,32 @@ export class ChatWidget extends Disposable implements IChatWidget { width = Math.min(width, 850); this.bodyDimension = new dom.Dimension(width, height); - const inputPartMaxHeight = this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height; - this.inputPart.layout(inputPartMaxHeight, width); - const inputPartHeight = this.inputPart.inputPartHeight; + this.inputPart.layout(this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height, width); + const inputHeight = this.inputPart.inputPartHeight; const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2; - const listHeight = Math.max(0, height - inputPartHeight); + const contentHeight = Math.max(0, height - inputHeight); if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { this.listContainer.style.removeProperty('--chat-current-response-min-height'); } else { - this.listContainer.style.setProperty('--chat-current-response-min-height', listHeight * .75 + 'px'); + this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px'); } - this.tree.layout(listHeight, width); - this.tree.getHTMLElement().style.height = `${listHeight}px`; + this.tree.layout(contentHeight, width); + this.tree.getHTMLElement().style.height = `${contentHeight}px`; - // Push the welcome message down so it doesn't change position when followups or working set appear - let extraOffset: number = 0; + // Push the welcome message down so it doesn't change position + // when followups, attachments or working set appear + let welcomeOffset = 100; if (this.viewOptions.renderFollowups) { - extraOffset = Math.max(100 - this.inputPart.followupsHeight, 0); - } else if (this.viewOptions.enableWorkingSet) { - extraOffset = Math.max(100 - this.inputPart.editSessionWidgetHeight, 0); + welcomeOffset = Math.max(welcomeOffset - this.inputPart.followupsHeight, 0); + } + if (this.viewOptions.enableWorkingSet) { + welcomeOffset = Math.max(welcomeOffset - this.inputPart.editSessionWidgetHeight, 0); } - this.welcomeMessageContainer.style.height = `${listHeight - extraOffset}px`; - this.welcomeMessageContainer.style.paddingBottom = `${extraOffset}px`; + welcomeOffset = Math.max(welcomeOffset - this.inputPart.attachmentsHeight, 0); + this.welcomeMessageContainer.style.height = `${contentHeight - welcomeOffset}px`; + this.welcomeMessageContainer.style.paddingBottom = `${welcomeOffset}px`; this.renderer.layout(width); const lastItem = this.viewModel?.getItems().at(-1); @@ -1256,7 +1391,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.scrollToEnd(); } - this.listContainer.style.height = `${listHeight}px`; + this.listContainer.style.height = `${contentHeight}px`; this._onDidChangeHeight.fire(height); } @@ -1362,11 +1497,6 @@ export class ChatWidget extends Disposable implements IChatWidget { saveState(): void { this.inputPart.saveState(); - - const welcomeContent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode)?.metadata.welcomeMessageContent; - if (welcomeContent) { - this.storageService.store(`${PersistWelcomeMessageContentKey}.${this.location}`, welcomeContent, StorageScope.APPLICATION, StorageTarget.MACHINE); - } } getViewState(): IChatViewState { @@ -1380,8 +1510,154 @@ export class ChatWidget extends Disposable implements IChatWidget { const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart); this.agentInInput.set(!!currentAgent); } + + /** + * Set's up the `chat mode` and selects required `tools` based on + * the metadata defined in headers of attached prompt files. + */ + private async setupChatModeAndTools( + input: string, + attachedContext: readonly IChatRequestVariableEntry[], + ): Promise { + // process prompt files starting from the 'root' ones + const promptFileVariables = attachedContext + .filter(isPromptFileChatVariable) + .filter(pick('isRoot')); + const promptUris = promptFileVariables.map(toUri); + + if (promptFileVariables.length === 0) { + return input; + } + + if (!input.trim()) { + const promptNames = (promptUris.length === 1) + ? `'${basename(promptUris[0])}'` + : `the prompt files`; + + input = `Follow instructions from ${promptNames}.`; + } + + + const metadata = await this.promptsService + .getCombinedToolsMetadata(promptUris); + + if (metadata === null) { + return input; + } + + const { mode, tools } = metadata; + + // switch to appropriate chat mode if needed + if (mode && mode !== this.inputPart.currentMode) { + await this.commandService.executeCommand( + ToggleAgentModeActionId, + { mode } satisfies IToggleChatModeArgs, + ); + } + + // if not tools to enable are present, we are done + if (tools === undefined) { + return input; + } + + // sanity check on the logic of the `getPromptFilesMetadata` method + // and the code above in case this block is moved around somewhere else: + // if we have some tools present, the mode must have been equal to `agent` + assert( + this.inputPart.currentMode === ChatMode.Agent, + `Chat mode must be 'agent' when there are 'tools' defined, got ${this.inputPart.currentMode}.`, + ); + + // convert tools names to tool IDs + const toolIds = tools + .map((toolName) => { + const tool = this.toolsService.getToolByName(toolName); + + if (tool === undefined) { + this.logService.warn( + `[setup tools]: cannot to find tool '${toolName}'`, + ); + } + + return tool; + }) + .filter(isDefined) + .map(pick('id')); + + // if there are some tools defined in the prompt files, select only the specified tools + this.inputPart + .selectedToolsModel + .selectOnly(toolIds); + + return input; + } + + /** + * Resolves instructions that have `include` metadata that can + * match file references in the attached context and then attaches + * such instructions to the context. + */ + private async autoAttachInstructions( + attachedContext: IChatRequestVariableEntry[], + ): Promise { + const variableUris = attachedContext + .filter(hasAddressableValue) + .map(toUri); + + const automaticInstructions = await this.promptsService + .findInstructionFilesFor(variableUris); + + // add instructions to the final context list + attachedContext.push( + ...automaticInstructions.map((uri) => { + return toChatVariable({ uri, isPromptFile: true }, true); + }), + ); + + // add to attached list to make the instructions sticky + this.inputPart + .attachmentModel + .promptInstructions.add(automaticInstructions); + } } +/** + * Type for any "addressable" object - i.e., an object that has + * the `value` property that is either a {@link URI} or a {@link Location}. + */ +export type TAddressable = T & { value: URI | Location }; + +/** + * Check if provided object is "addressable" - i.e., has the `value` + * property that is either a {@link URI} or a {@link Location}. + */ +const hasAddressableValue = ( + thing: T, +): thing is TAddressable => { + if ((!thing) || (('value' in thing) === false)) { + return false; + } + + if (URI.isUri(thing.value) || isLocation(thing.value)) { + return true; + } + + return false; +}; + +/** + * Returns URI of a provided "addressable" object. + */ +const toUri = ( + thing: TAddressable, +): URI => { + const { value } = thing; + + return URI.isUri(value) + ? value + : value.uri; +}; + export class ChatWidgetService extends Disposable implements IChatWidgetService { declare readonly _serviceBrand: undefined; diff --git a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css index 44f05c472b0..901afc3a57b 100644 --- a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -9,7 +9,8 @@ } .interactive-result-code-block .interactive-result-code-block-toolbar { - display: none; + opacity: 0; + pointer-events: none; } .interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar, @@ -48,12 +49,14 @@ .interactive-result-code-block:hover .interactive-result-code-block-toolbar, .interactive-result-code-block .interactive-result-code-block-toolbar:focus-within, .interactive-result-code-block.focused .interactive-result-code-block-toolbar { - display: initial; + opacity: 1; border-radius: 2px; + pointer-events: auto; } -.interactive-result-code-block .interactive-result-code-block-toolbar.force-visibility .monaco-toolbar { - display: initial !important; +.interactive-result-code-block .interactive-result-code-block-toolbar.force-visibility { + opacity: 1 !important; + pointer-events: auto !important; } .interactive-item-container .value .rendered-markdown [data-code] { diff --git a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 399a2f9e36f..9ef202f62c7 100644 --- a/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/code/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -89,6 +89,8 @@ export interface ICodeBlockData { readonly parentContextKeyService?: IContextKeyService; readonly renderOptions?: ICodeBlockRenderOptions; + + readonly chatSessionId: string; } /** @@ -135,6 +137,8 @@ export interface ICodeBlockActionContext { languageId?: string; codeBlockIndex: number; element: unknown; + + chatSessionId: string | undefined; } export interface ICodeBlockRenderOptions { @@ -451,6 +455,7 @@ export class CodeBlockPart extends Disposable { element: data.element, languageId: textModel.getLanguageId(), codemapperUri: data.codemapperUri, + chatSessionId: data.chatSessionId } satisfies ICodeBlockActionContext; this.resourceContextKey.set(textModel.uri); } @@ -704,7 +709,7 @@ export class CodeCompareBlockPart extends Disposable { const toolbarElt = this.toolbar.getElement(); if (this.accessibilityService.isScreenReaderOptimized()) { toolbarElt.style.display = 'block'; - toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); + toolbarElt.ariaLabel = localize('chat.codeBlock.toolbar', 'Code block toolbar'); } else { toolbarElt.style.display = ''; } diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 48ba9a62047..9423d3b995e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -3,40 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, groupBy } from '../../../../../base/common/arrays.js'; -import { assertNever } from '../../../../../base/common/assert.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; import * as glob from '../../../../../base/common/glob.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, dispose, isDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { basename, dirname, joinPath, relativePath } from '../../../../../base/common/resources.js'; +import { basename, dirname, extUri, joinPath, relativePath } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { Command, isLocation } from '../../../../../editor/common/languages.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { localize } from '../../../../../nls.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { FileType, IFileService } from '../../../../../platform/files/common/files.js'; +import { FileKind, FileType, IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; -import { IQuickAccessOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IHistoryService } from '../../../../services/history/common/history.js'; import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; -import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; -import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; -import { IChatRequestProblemsVariable, IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; +import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; @@ -61,6 +56,8 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC return ChatDynamicVariableModel.ID; } + private decorationData: { id: string; text: string }[] = []; + constructor( private readonly widget: IChatWidget, @ILabelService private readonly labelService: ILabelService, @@ -70,43 +67,62 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC super(); this._register(widget.inputEditor.onDidChangeModelContent(e => { - e.changes.forEach(c => { - // Don't mutate entries in _variables, since they will be returned from the getter - this._variables = coalesce(this._variables.map(ref => { - const intersection = Range.intersectRanges(ref.range, c.range); - if (intersection && !intersection.isEmpty()) { - // The reference text was changed, it's broken. - // But if the whole reference range was deleted (eg history navigation) then don't try to change the editor. - if (!Range.containsRange(c.range, ref.range)) { - const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); - this.widget.refreshParsedInput(); - } - - // dispose the reference if possible before dropping it off - if ('dispose' in ref && typeof ref.dispose === 'function') { - ref.dispose(); - } - - return null; - } else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) { - const delta = c.text.length - c.rangeLength; - ref.range = { - startLineNumber: ref.range.startLineNumber, - startColumn: ref.range.startColumn + delta, - endLineNumber: ref.range.endLineNumber, - endColumn: ref.range.endColumn + delta, - }; - - return ref; - } + const removed: TDynamicVariable[] = []; + let didChange = false; + + // Don't mutate entries in _variables, since they will be returned from the getter + this._variables = coalesce(this._variables.map((ref, idx): TDynamicVariable | null => { + const model = widget.inputEditor.getModel(); + + if (!model) { + removed.push(ref); + return null; + } + + const data = this.decorationData[idx]; + const newRange = model.getDecorationRange(data.id); + + if (!newRange) { + // gone + removed.push(ref); + return null; + } + + const newText = model.getValueInRange(newRange); + if (newText !== data.text) { + + this.widget.inputEditor.executeEdits(this.id, [{ + range: newRange, + text: '', + }]); + this.widget.refreshParsedInput(); + + removed.push(ref); + return null; + } + + if (newRange.equalsRange(ref.range)) { + // all good return ref; - })); - }); + } + + didChange = true; + + if (ref instanceof ChatFileReference) { + ref.range = newRange; + return ref; + } else { + return { ...ref, range: newRange }; + } + })); + + // cleanup disposable variables + dispose(removed.filter(isDisposable)); + + if (didChange || removed.length > 0) { + this.widget.refreshParsedInput(); + } this.updateDecorations(); })); @@ -165,10 +181,19 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC } private updateDecorations(): void { - this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({ + + const decorationIds = this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({ range: r.range, hoverMessage: this.getHoverForReference(r) }))); + + this.decorationData = []; + for (let i = 0; i < decorationIds.length; i++) { + this.decorationData.push({ + id: decorationIds[i], + text: this.widget.inputEditor.getModel()!.getValueInRange(this._variables[i].range) + }); + } } private getHoverForReference(ref: IDynamicVariable): IMarkdownString | undefined { @@ -189,7 +214,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC */ private disposeVariables(): void { for (const variable of this._variables) { - if ('dispose' in variable && typeof variable.dispose === 'function') { + if (isDisposable(variable)) { variable.dispose(); } } @@ -213,166 +238,30 @@ function isDynamicVariable(obj: any): obj is IDynamicVariable { ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); -interface SelectAndInsertActionContext { - widget: IChatWidget; - range: IRange; -} - -function isSelectAndInsertActionContext(context: any): context is SelectAndInsertActionContext { - return 'widget' in context && 'range' in context; -} - -export class SelectAndInsertFileAction extends Action2 { - static readonly Name = 'files'; - static readonly Item = { - label: localize('allFiles', 'All Files'), - description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), - }; - static readonly ID = 'workbench.action.chat.selectAndInsertFile'; - - constructor() { - super({ - id: SelectAndInsertFileAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const textModelService = accessor.get(ITextModelService); - const logService = accessor.get(ILogService); - const quickInputService = accessor.get(IQuickInputService); - - const context = args[0]; - if (!isSelectAndInsertActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `file` - context.widget.inputEditor.executeEdits('chatInsertFile', [{ range: context.range, text: `` }]); - }; - - let options: IQuickAccessOptions | undefined; - // TODO: have dedicated UX for this instead of using the quick access picker - const picks = await quickInputService.quickAccess.pick('', options); - if (!picks?.length) { - logService.trace('SelectAndInsertFileAction: no file selected'); - doCleanup(); - return; - } - - const editor = context.widget.inputEditor; - const range = context.range; - - // Handle the special case of selecting all files - if (picks[0] === SelectAndInsertFileAction.Item) { - const text = `#${SelectAndInsertFileAction.Name}`; - const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); - doCleanup(); - } - return; - } - - // Handle the case of selecting a specific file - const resource = (picks[0] as unknown as { resource: unknown }).resource as URI; - if (!textModelService.canHandleResource(resource)) { - logService.trace('SelectAndInsertFileAction: non-text resource selected'); - doCleanup(); - return; - } - - const fileName = basename(resource); - const text = `#file:${fileName}`; - const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.file', - isFile: true, - prefix: 'file', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: resource - }); - } -} -registerAction2(SelectAndInsertFileAction); - -export class SelectAndInsertFolderAction extends Action2 { - static readonly Name = 'folder'; - static readonly ID = 'workbench.action.chat.selectAndInsertFolder'; - - constructor() { - super({ - id: SelectAndInsertFolderAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const logService = accessor.get(ILogService); - - const context = args[0]; - if (!isSelectAndInsertActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `folder` - context.widget.inputEditor.executeEdits('chatInsertFolder', [{ range: context.range, text: `` }]); - }; - - const folder = await createFolderQuickPick(accessor); - if (!folder) { - logService.trace('SelectAndInsertFolderAction: no folder selected'); - doCleanup(); - return; - } - - const editor = context.widget.inputEditor; - const range = context.range; - - const folderName = basename(folder); - const text = `#folder:${folderName}`; - const success = editor.executeEdits('chatInsertFolder', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertFolderAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.folder', - isFile: false, - isDirectory: true, - prefix: 'folder', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: folder - }); - } - -} -registerAction2(SelectAndInsertFolderAction); -export async function createFolderQuickPick(accessor: ServicesAccessor): Promise { +export async function createFilesAndFolderQuickPick(accessor: ServicesAccessor): Promise { const quickInputService = accessor.get(IQuickInputService); const searchService = accessor.get(ISearchService); const configurationService = accessor.get(IConfigurationService); const workspaceService = accessor.get(IWorkspaceContextService); const fileService = accessor.get(IFileService); const labelService = accessor.get(ILabelService); + const modelService = accessor.get(IModelService); + const languageService = accessor.get(ILanguageService); + const historyService = accessor.get(IHistoryService); + + type ResourcePick = IQuickPickItem & { resource: URI; kind: FileKind }; const workspaces = workspaceService.getWorkspace().folders.map(folder => folder.uri); - const topLevelFolderItems = (await getTopLevelFolders(workspaces, fileService)).map(createQuickPickItem); - const quickPick = quickInputService.createQuickPick(); - quickPick.placeholder = 'Search folder by name'; - quickPick.items = topLevelFolderItems; + const defaultItems: ResourcePick[] = []; + (await getTopLevelFolders(workspaces, fileService)).forEach(uri => defaultItems.push(createQuickPickItem(uri, FileKind.FOLDER))); + historyService.getHistory().filter(a => a.resource).slice(0, 30).forEach(uri => defaultItems.push(createQuickPickItem(uri.resource!, FileKind.FILE))); + defaultItems.sort((a, b) => extUri.compare(a.resource, b.resource)); + + const quickPick = quickInputService.createQuickPick(); + quickPick.placeholder = 'Search file or folder by name'; + quickPick.items = defaultItems; return await new Promise(_resolve => { @@ -385,24 +274,32 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise disposables.add(quickPick.onDidChangeValue(async value => { if (value === '') { - quickPick.items = topLevelFolderItems; + quickPick.items = defaultItems; return; } - const workspaceFolders = await Promise.all( - workspaces.map(workspace => - searchFolders( - workspace, - value, - true, - undefined, - undefined, - configurationService, - searchService - ) - )); - - quickPick.items = workspaceFolders.flat().map(createQuickPickItem); + const picks: ResourcePick[] = []; + + await Promise.all(workspaces.map(async workspace => { + const result = await searchFilesAndFolders( + workspace, + value, + true, + undefined, + undefined, + configurationService, + searchService + ); + + for (const folder of result.folders) { + picks.push(createQuickPickItem(folder, FileKind.FOLDER)); + } + for (const file of result.files) { + picks.push(createQuickPickItem(file, FileKind.FILE)); + } + })); + + quickPick.items = picks.sort((a, b) => extUri.compare(a.resource, b.resource)); })); disposables.add(quickPick.onDidAccept((e) => { @@ -417,15 +314,16 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise quickPick.show(); }); - function createQuickPickItem(folder: URI): IQuickPickItem & { resource: URI } { + function createQuickPickItem(resource: URI, kind: FileKind): ResourcePick { return { - type: 'item', - id: folder.toString(), - resource: folder, + resource, + kind, + id: resource.toString(), alwaysShow: true, - label: basename(folder), - description: labelService.getUriLabel(dirname(folder), { relative: true }), - iconClass: ThemeIcon.asClassName(Codicon.folder), + label: basename(resource), + description: labelService.getUriLabel(dirname(resource), { relative: true }), + iconClasses: kind === FileKind.FILE ? getIconClasses(modelService, languageService, resource, FileKind.FILE) : undefined, + iconClass: kind === FileKind.FOLDER ? ThemeIcon.asClassName(Codicon.folder) : undefined }; } } @@ -450,7 +348,7 @@ export async function getTopLevelFolders(workspaces: URI[], fileService: IFileSe return folders; } -export async function searchFolders( +export async function searchFilesAndFolders( workspace: URI, pattern: string, fuzzyMatch: boolean, @@ -458,7 +356,7 @@ export async function searchFolders( cacheKey: string | undefined, configurationService: IConfigurationService, searchService: ISearchService -): Promise { +): Promise<{ folders: URI[]; files: URI[] }> { const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; @@ -471,23 +369,26 @@ export async function searchFolders( shouldGlobMatchFilePattern: true, cacheKey, excludePattern: searchExcludePattern, + sortByScore: true, }; - let folderResults: ISearchComplete | undefined; + let searchResult: ISearchComplete | undefined; try { - folderResults = await searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}/**` }, token); + searchResult = await searchService.fileSearch({ ...searchOptions, filePattern: `{**/${segmentMatchPattern}/**,${pattern}}` }, token); } catch (e) { if (!isCancellationError(e)) { throw e; } } - if (!folderResults || token?.isCancellationRequested) { - return []; + if (!searchResult || token?.isCancellationRequested) { + return { files: [], folders: [] }; } - const folderResources = getMatchingFoldersFromFiles(folderResults.results.map(result => result.resource), workspace, segmentMatchPattern); - return folderResources; + const fileResources = searchResult.results.map(result => result.resource); + const folderResources = getMatchingFoldersFromFiles(fileResources, workspace, segmentMatchPattern); + + return { folders: folderResources, files: fileResources }; } function fuzzyMatchingGlobPattern(pattern: string): string { @@ -549,68 +450,6 @@ function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMa return matchingFolders; } -export class SelectAndInsertSymAction extends Action2 { - static readonly Name = 'symbols'; - static readonly ID = 'workbench.action.chat.selectAndInsertSym'; - - constructor() { - super({ - id: SelectAndInsertSymAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const textModelService = accessor.get(ITextModelService); - const logService = accessor.get(ILogService); - const quickInputService = accessor.get(IQuickInputService); - - const context = args[0]; - if (!isSelectAndInsertActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `sym` - context.widget.inputEditor.executeEdits('chatInsertSym', [{ range: context.range, text: `` }]); - }; - - // TODO: have dedicated UX for this instead of using the quick access picker - const picks = await quickInputService.quickAccess.pick('#', { enabledProviderPrefixes: ['#'] }); - if (!picks?.length) { - logService.trace('SelectAndInsertSymAction: no symbol selected'); - doCleanup(); - return; - } - - const editor = context.widget.inputEditor; - const range = context.range; - - // Handle the case of selecting a specific file - const symbol = (picks[0] as ISymbolQuickPickItem).symbol; - if (!symbol || !textModelService.canHandleResource(symbol.location.uri)) { - logService.trace('SelectAndInsertSymAction: non-text resource selected'); - doCleanup(); - return; - } - - const text = `#sym:${symbol.name}`; - const success = editor.executeEdits('chatInsertSym', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertSymAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.symbol', - prefix: 'symbol', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: symbol.location - }); - } -} -registerAction2(SelectAndInsertSymAction); export interface IAddDynamicVariableContext { id: string; @@ -676,139 +515,8 @@ export class AddDynamicVariableAction extends Action2 { id: context.id, range: range, isFile: true, - prefix: 'file', data: variableData }); } } registerAction2(AddDynamicVariableAction); - -export async function createMarkersQuickPick(accessor: ServicesAccessor, level: 'problem' | 'file', onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { - const markers = accessor.get(IMarkerService).read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); - if (!markers.length) { - return; - } - - const uriIdentityService = accessor.get(IUriIdentityService); - const labelService = accessor.get(ILabelService); - const grouped = groupBy(markers, (a, b) => uriIdentityService.extUri.compare(a.resource, b.resource)); - - const severities = new Set(); - type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; - const items: (MarkerPickItem | IQuickPickSeparator)[] = []; - - let pickCount = 0; - for (const group of grouped) { - const resource = group[0].resource; - if (level === 'problem') { - items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); - for (const marker of group) { - pickCount++; - severities.add(marker.severity); - items.push({ - type: 'item', - resource: marker.resource, - label: marker.message, - description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), - entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), - }); - } - } else if (level === 'file') { - const entry = { filterUri: resource }; - pickCount++; - items.push({ - type: 'item', - resource, - label: IDiagnosticVariableEntryFilterData.label(entry), - description: group[0].message + (group.length > 1 ? localize('problemsMore', '+ {0} more', group.length - 1) : ''), - entry, - }); - for (const marker of group) { - severities.add(marker.severity); - } - } else { - assertNever(level); - } - } - - if (pickCount < 2) { // single error in a URI - return items.find((i): i is MarkerPickItem => i.type === 'item')?.entry; - } - - if (level === 'file') { - items.unshift({ type: 'separator', label: localize('markers.panel.files', 'Files') }); - } - - items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); - - const quickInputService = accessor.get(IQuickInputService); - const store = new DisposableStore(); - const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); - quickPick.canAcceptInBackground = !onBackgroundAccept; - quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); - quickPick.items = items; - - return new Promise(resolve => { - store.add(quickPick.onDidHide(() => resolve(undefined))); - store.add(quickPick.onDidAccept(ev => { - if (ev.inBackground) { - onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); - } else { - resolve(quickPick.selectedItems[0]?.entry); - quickPick.dispose(); - } - })); - quickPick.show(); - }).finally(() => store.dispose()); -} - -export class SelectAndInsertProblemAction extends Action2 { - static readonly Name = 'problems'; - static readonly ID = 'workbench.action.chat.selectAndInsertProblems'; - - constructor() { - super({ - id: SelectAndInsertProblemAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const logService = accessor.get(ILogService); - const context = args[0]; - if (!isSelectAndInsertActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `problem` - context.widget.inputEditor.executeEdits('chatInsertProblems', [{ range: context.range, text: `` }]); - }; - - const pick = await createMarkersQuickPick(accessor, 'file'); - if (!pick) { - doCleanup(); - return; - } - - const editor = context.widget.inputEditor; - const originalRange = context.range; - const insertText = `#${SelectAndInsertProblemAction.Name}:${pick.filterUri ? basename(pick.filterUri) : MarkerSeverity.toString(pick.filterSeverity!)}`; - - const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length); - const success = editor.executeEdits('chatInsertProblems', [{ range: varRange, text: insertText + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertProblemsAction: failed to insert "${insertText}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.problems', - prefix: SelectAndInsertProblemAction.Name, - range: varRange, - data: { id: 'vscode.problems', filter: pick } satisfies IChatRequestProblemsVariable, - }); - } -} -registerAction2(SelectAndInsertProblemAction); diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts index cb6ce3b9d2d..7fe2a6d72dd 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables/chatFileReference.ts @@ -9,6 +9,7 @@ import { IDynamicVariable } from '../../../common/chatVariables.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** @@ -23,6 +24,7 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria constructor( public readonly reference: IDynamicVariable, @IInstantiationService initService: IInstantiationService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { const { data } = reference; @@ -32,7 +34,7 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria `Variable data must be an URI, got '${data}'.`, ); - super(data, [], initService, logService); + super(data, {}, initService, workspaceService, logService); } /** @@ -57,10 +59,6 @@ export class ChatFileReference extends FilePromptParser implements IDynamicVaria return this.uri; } - public get prefix() { - return this.reference.prefix; - } - public get isFile() { return this.reference.isFile; } diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index afcd62f2218..9b532a8c7b4 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -8,22 +8,27 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { basename } from '../../../../../base/common/resources.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Location } from '../../../../../editor/common/languages.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { IBaseChatRequestVariableEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js'; +import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; +import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/constants.js'; +import { IPromptsService, TSharedPrompt } from '../../common/promptSyntax/service/types.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { toChatVariable } from '../chatAttachmentModel/chatPromptAttachmentsCollection.js'; export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'chat.implicitContext'; @@ -55,13 +60,29 @@ export class ChatImplicitContextContribution extends Disposable implements IWork Event.any( codeEditor.onDidChangeModel, codeEditor.onDidChangeCursorSelection, - codeEditor.onDidScrollChange), + codeEditor.onDidScrollChange, + codeEditor.onDidChangeModelLanguage), () => undefined, 500)(() => this.updateImplicitContext())); } const notebookEditor = this.findActiveNotebookEditor(); if (notebookEditor) { + const activeCellDisposables = activeEditorDisposables.add(new DisposableStore()); + activeEditorDisposables.add(notebookEditor.onDidChangeActiveCell(() => { + activeCellDisposables.clear(); + const codeEditor = this.codeEditorService.getActiveCodeEditor(); + if (codeEditor && codeEditor.getModel()?.uri.scheme === Schemas.vscodeNotebookCell) { + activeCellDisposables.add(Event.debounce( + Event.any( + codeEditor.onDidChangeModel, + codeEditor.onDidChangeCursorSelection, + codeEditor.onDidScrollChange), + () => undefined, + 500)(() => this.updateImplicitContext())); + } + })); + activeEditorDisposables.add(Event.debounce( Event.any( notebookEditor.onDidChangeModel, @@ -89,7 +110,7 @@ export class ChatImplicitContextContribution extends Disposable implements IWork return; } if (this._implicitContextEnablement[widget.location] === 'first' && widget.viewModel?.getItems().length !== 0) { - widget.input.implicitContext.setValue(undefined, false); + widget.input.implicitContext.setValue(undefined, false, undefined); } })); this._register(this.chatWidgetService.onDidAddWidget(async (widget) => { @@ -138,7 +159,10 @@ export class ChatImplicitContextContribution extends Disposable implements IWork const selection = codeEditor?.getSelection(); let newValue: Location | URI | undefined; let isSelection = false; + + let languageId: string | undefined; if (model) { + languageId = model.getLanguageId(); if (selection && !selection.isEmpty()) { newValue = { uri: model.uri, range: selection } satisfies Location; isSelection = true; @@ -162,14 +186,34 @@ export class ChatImplicitContextContribution extends Disposable implements IWork if (notebookEditor) { const activeCell = notebookEditor.getActiveCell(); if (activeCell) { + const codeEditor = this.codeEditorService.getActiveCodeEditor(); + const selection = codeEditor?.getSelection(); + const visibleRanges = codeEditor?.getVisibleRanges() || []; newValue = activeCell.uri; + if (isEqual(codeEditor?.getModel()?.uri, activeCell.uri)) { + if (selection && !selection.isEmpty()) { + newValue = { uri: activeCell.uri, range: selection } satisfies Location; + isSelection = true; + } else if (visibleRanges.length > 0) { + // Merge visible ranges. Maybe the reference value could actually be an array of Locations? + // Something like a Location with an array of Ranges? + let range = visibleRanges[0]; + visibleRanges.slice(1).forEach(r => { + range = range.plusRange(r); + }); + newValue = { uri: activeCell.uri, range } satisfies Location; + } + } } else { newValue = notebookEditor.textModel?.uri; } } const uri = newValue instanceof URI ? newValue : newValue?.uri; - if (uri && await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token)) { + if (uri && ( + await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token) || + uri.path.endsWith('.copilotmd')) + ) { newValue = undefined; } @@ -177,7 +221,7 @@ export class ChatImplicitContextContribution extends Disposable implements IWork return; } - const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditingSession), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Editor)]; + const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Editor)]; for (const widget of widgets) { if (!widget.input.implicitContext) { continue; @@ -185,18 +229,30 @@ export class ChatImplicitContextContribution extends Disposable implements IWork const setting = this._implicitContextEnablement[widget.location]; const isFirstInteraction = widget.viewModel?.getItems().length === 0; if (setting === 'first' && !isFirstInteraction) { - widget.input.implicitContext.setValue(undefined, false); + widget.input.implicitContext.setValue(undefined, false, undefined); } else if (setting === 'always' || setting === 'first' && isFirstInteraction) { - widget.input.implicitContext.setValue(newValue, isSelection); + widget.input.implicitContext.setValue(newValue, isSelection, languageId); } else if (setting === 'never') { - widget.input.implicitContext.setValue(undefined, false); + widget.input.implicitContext.setValue(undefined, false, undefined); } } } } export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry { + /** + * If the implicit context references a prompt file, this field + * holds a reference to an associated prompt parser instance. + */ + private prompt: TSharedPrompt | undefined; + get id() { + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); + + return variable.id; + } + if (URI.isUri(this.value)) { return 'vscode.implicit.file'; } else if (this.value) { @@ -211,6 +267,12 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } get name(): string { + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); + + return variable.name; + } + if (URI.isUri(this.value)) { return `file:${basename(this.value)}`; } else if (this.value) { @@ -223,6 +285,12 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli readonly kind = 'implicit'; get modelDescription(): string { + if (this.prompt !== undefined) { + const variable = toChatVariable(this.prompt, true); + + return variable.modelDescription; + } + if (URI.isUri(this.value)) { return `User's active file`; } else if (this._isSelection) { @@ -239,7 +307,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli return this._isSelection; } - private _onDidChangeValue = new Emitter(); + private _onDidChangeValue = this._register(new Emitter()); readonly onDidChangeValue = this._onDidChangeValue.event; private _value: Location | URI | undefined; @@ -257,24 +325,97 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli this._onDidChangeValue.fire(); } - constructor(value?: Location | URI) { + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @IModelService private readonly modelService: IModelService, + @ILogService private readonly logService: ILogService, + ) { super(); - this._value = value; } - setValue(value: Location | URI | undefined, isSelection: boolean) { + setValue(value: Location | URI | undefined, isSelection: boolean, languageId?: string): void { this._value = value; this._isSelection = isSelection; + + // remove and dispose existent prompt parser instance + this.removePrompt(); + // if language ID is a 'prompt' language, create a prompt parser instance + if (value && (languageId === PROMPT_LANGUAGE_ID)) { + this.addPrompt(value); + } + this._onDidChangeValue.fire(); } - toBaseEntry(): IBaseChatRequestVariableEntry { - return { - id: this.id, - name: this.name, - value: this.value, - isFile: true, - modelDescription: this.modelDescription - }; + public async toBaseEntries(): Promise { + // chat variable for non-prompt file attachment + if (this.prompt === undefined) { + return [{ + kind: 'file', + id: this.id, + name: this.name, + value: this.value, + modelDescription: this.modelDescription, + }]; + + } + + // prompt can have any number of nested references, hence + // collect all of valid ones and return the entire list + await this.prompt.allSettled(); + return [ + // add all valid child references in the prompt + ...this.prompt.allValidReferences.map((link) => { + return toChatVariable(link, false); + }), + // and then the root prompt reference itself + toChatVariable({ + uri: this.prompt.uri, + // the attached file must have been a prompt file therefore + // we force that assumption here; this makes sure that prompts + // in untitled documents can be also attached to the chat input + isPromptFile: true, + }, true), + ]; + } + + /** + * Whether the implicit context references a prompt file. + */ + public get isPromptFile() { + return (this.prompt !== undefined); + } + + /** + * Add prompt parser instance for the provided value. + */ + private addPrompt( + value: URI | Location, + ): void { + const uri = URI.isUri(value) + ? value + : value.uri; + + const model = this.modelService.getModel(uri); + const modelExists = (model !== null); + if ((modelExists === false) || model.isDisposed()) { + return this.logService.warn( + `cannot create prompt parser instance for ${uri.path} (model exists: ${modelExists})`, + ); + } + + this.prompt = this.promptsService.getSyntaxParserFor(model); + } + + /** + * Remove and dispose prompt parser instance. + */ + private removePrompt(): void { + delete this.prompt; + } + + public override dispose(): void { + this.removePrompt(); + super.dispose(); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 804fd5765d6..791c3d5b656 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -9,7 +9,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { isPatternInWord } from '../../../../../base/common/filters.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -24,35 +23,34 @@ import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { FileKind } from '../../../../../platform/files/common/files.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHistoryService } from '../../../../services/history/common/history.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; import { ISearchService } from '../../../../services/search/common/search.js'; import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IDynamicVariable } from '../../common/chatVariables.js'; import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; -import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { ChatEditingSessionSubmitAction, ChatSubmitAction } from '../actions/chatExecuteActions.js'; +import { IPromptsService } from '../../common/promptSyntax/service/types.js'; +import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, SelectAndInsertFileAction, SelectAndInsertFolderAction, SelectAndInsertProblemAction, SelectAndInsertSymAction, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, searchFilesAndFolders } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, + @IPromptsService private readonly promptsService: IPromptsService, ) { super(); @@ -97,7 +95,7 @@ class SlashCommandCompletions extends Disposable { range, sortText: c.sortText ?? 'a'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: widget.location === ChatAgentLocation.EditingSession ? ChatEditingSessionSubmitAction.ID : ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, }; }) }; @@ -138,7 +136,54 @@ class SlashCommandCompletions extends Disposable { filterText: `${chatAgentLeader}${c.command}`, sortText: c.sortText ?? 'z'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: widget.location === ChatAgentLocation.EditingSession ? ChatEditingSessionSubmitAction.ID : ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + }; + }) + }; + } + })); + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'promptSlashCommands', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel) { + return null; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + if (!isEmptyUpToCompletionWord(model, range)) { + // No text allowed before the completion + return; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + + const promptCommands = await this.promptsService.findPromptSlashCommands(); + if (promptCommands.length === 0) { + return null; + } + + return { + suggestions: promptCommands.map((c, i): CompletionItem => { + const label = `/${c.command}`; + const description = c.promptPath?.storage === 'user' ? localize('promptFileDescription', 'User Prompt File') : localize('promptFileDescriptionWorkspace', 'Workspace Prompt File'); + return { + label: { label, description }, + insertText: `${label} `, + documentation: c.detail, + range, + sortText: 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, }; }) }; @@ -179,8 +224,8 @@ class AgentCompletions extends Disposable { return; } - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { + const usedOtherCommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashPromptPart); + if (usedOtherCommand) { // Only one allowed return; } @@ -452,9 +497,8 @@ interface IVariableCompletionsDetails { class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:]*`, 'g'); // MUST be using `g`-flag + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:-]*`, 'g'); // MUST be using `g`-flag - private readonly queryBuilder: QueryBuilder; constructor( @IHistoryService private readonly historyService: IHistoryService, @@ -464,68 +508,23 @@ class BuiltinDynamicCompletions extends Disposable { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IOutlineModelService private readonly outlineService: IOutlineModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, - @IMarkerService markerService: IMarkerService, ) { super(); - // File completions - this.registerVariableCompletions('file', async ({ widget, range, position, model }, token) => { + // File/Folder completions in one go and m + const fileWordPattern = new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'); + this.registerVariableCompletions('fileAndFolder', async ({ widget, range }, token) => { if (!widget.supportsFileReferences) { - return null; + return; } - const result: CompletionList = { suggestions: [] }; - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - documentation: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - }); - - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFileEntries(widget, result, range2, token); - } - + await this.addFileAndFolderEntries(widget, result, range, token); return result; - }); - - // Folder completions - this.registerVariableCompletions('folder', async ({ widget, range, position, model }, token) => { - if (!widget.supportsFileReferences) { - return null; - } - - const result: CompletionList = { suggestions: [] }; - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#folder:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}folder`, - insertText: `${chatVariableLeader}folder:`, - documentation: localize('pickFolderLabel', "Pick a folder"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFolderAction.ID, title: SelectAndInsertFolderAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - }); - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); - if (range2) { - await this.addFolderEntries(widget, result, range2, token); - } - - return result; - }); + }, fileWordPattern); // Selection completion this.registerVariableCompletions('selection', ({ widget, range }, token) => { @@ -564,7 +563,6 @@ class BuiltinDynamicCompletions extends Disposable { command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { id: 'vscode.selection', - prefix: 'file', isFile: true, range: { startLineNumber: range.replace.startLineNumber, startColumn: range.replace.startColumn, endLineNumber: range.replace.endLineNumber, endColumn: range.replace.startColumn + text.length }, data: { range: currentSelection, uri: currentResource } satisfies Location @@ -581,18 +579,6 @@ class BuiltinDynamicCompletions extends Disposable { } const result: CompletionList = { suggestions: [] }; - - const afterRangeSym = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#sym:'.length); - result.suggestions.push({ - label: `${chatVariableLeader}sym`, - insertText: `${chatVariableLeader}sym:`, - documentation: localize('pickSymbolLabel', "Pick a symbol"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertSymAction.ID, title: SelectAndInsertSymAction.ID, arguments: [{ widget, range: afterRangeSym }] }, - sortText: 'z' - }); - const range2 = computeCompletionRanges(model, position, new RegExp(`${chatVariableLeader}[^\\s]*`, 'g'), true); if (range2) { this.addSymbolEntries(widget, result, range2, token); @@ -601,36 +587,10 @@ class BuiltinDynamicCompletions extends Disposable { return result; }); - // Problems completions, we just attach all problems in this case - this.registerVariableCompletions(SelectAndInsertProblemAction.Name, ({ widget, range, position, model }, token) => { - const stats = markerService.getStatistics(); - if (!stats.errors && !stats.warnings) { - return null; - } - - const result: CompletionList = { suggestions: [] }; - - const completedText = `${chatVariableLeader}${SelectAndInsertProblemAction.Name}:`; - const afterTextRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + completedText.length); - result.suggestions.push({ - label: `${chatVariableLeader}${SelectAndInsertProblemAction.Name}`, - insertText: completedText, - documentation: localize('pickProblemsLabel', "Problems in your workspace"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertProblemAction.ID, title: SelectAndInsertProblemAction.ID, arguments: [{ widget, range: afterTextRange }] }, - sortText: 'z' - }); - - return result; - }); - this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); - - this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } - private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult) { + private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult, wordPattern: RegExp = BuiltinDynamicCompletions.VariableNameDef) { this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: `chatVarCompletions-${debugName}`, triggerCharacters: [chatVariableLeader], @@ -640,7 +600,7 @@ class BuiltinDynamicCompletions extends Disposable { return; } - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef, true); + const range = computeCompletionRanges(model, position, wordPattern, true); if (range) { return provider({ model, position, widget, range, context }, token); } @@ -652,9 +612,9 @@ class BuiltinDynamicCompletions extends Disposable { private cacheKey?: { key: string; time: number }; - private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { + private async addFileAndFolderEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { - const makeFileCompletionItem = (resource: URI, description?: string): CompletionItem => { + const makeCompletionItem = (resource: URI, kind: FileKind, description?: string): CompletionItem => { const basename = this.labelService.getUriBasenameLabel(resource); const text = `${chatVariableLeader}file:${basename}`; @@ -669,13 +629,13 @@ class BuiltinDynamicCompletions extends Disposable { filterText: `${chatVariableLeader}${basename}`, insertText: info.varWord?.endColumn === info.replace.endColumn ? `${text} ` : text, range: info, - kind: CompletionItemKind.File, + kind: kind === FileKind.FILE ? CompletionItemKind.File : CompletionItemKind.Folder, sortText, command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.file', - prefix: 'file', - isFile: true, + id: resource.toString(), + isFile: kind === FileKind.FILE, + isDirectory: kind === FileKind.FOLDER, range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, data: resource })] @@ -696,11 +656,10 @@ class BuiltinDynamicCompletions extends Disposable { const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { - if (seen.has(relatedFile.uri)) { - continue; + if (!seen.has(relatedFile.uri)) { + seen.add(relatedFile.uri); + result.suggestions.push(makeCompletionItem(relatedFile.uri, FileKind.FILE, relatedFile.description)); } - seen.add(relatedFile.uri); - result.suggestions.push(makeFileCompletionItem(relatedFile.uri, relatedFile.description)); } } } @@ -708,8 +667,8 @@ class BuiltinDynamicCompletions extends Disposable { // HISTORY // always take the last N items for (const item of this.historyService.getHistory()) { - if (!item.resource || !this.workspaceContextService.getWorkspaceFolder(item.resource)) { - // ignore "forgein" editors + if (!item.resource || seen.has(item.resource)) { + // ignore editors without a resource continue; } @@ -722,7 +681,7 @@ class BuiltinDynamicCompletions extends Disposable { } seen.add(item.resource); - const newLen = result.suggestions.push(makeFileCompletionItem(item.resource)); + const newLen = result.suggestions.push(makeCompletionItem(item.resource, FileKind.FILE)); if (newLen - len >= 5) { break; } @@ -733,90 +692,22 @@ class BuiltinDynamicCompletions extends Disposable { if (pattern) { const cacheKey = this.updateCacheKey(); - - const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { - filePattern: pattern, - sortByScore: true, - maxResults: 250, - cacheKey: cacheKey.key - }); - - const data = await this.searchService.fileSearch(query, token); - for (const match of data.results) { - if (seen.has(match.resource)) { - // already included via history - continue; - } - result.suggestions.push(makeFileCompletionItem(match.resource)); - } - } - - // mark results as incomplete because further typing might yield - // in more search results - result.incomplete = true; - } - - private async addFolderEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { - - const folderLeader = `${chatVariableLeader}folder:`; - - const makeFolderCompletionItem = (resource: URI, description?: string): CompletionItem => { - - const basename = this.labelService.getUriBasenameLabel(resource); - const text = `${folderLeader}${basename}`; - const uriLabel = this.labelService.getUriLabel(dirname(resource), { relative: true }); - const labelDescription = description - ? localize('folderEntryDescription', '{0} ({1})', uriLabel, description) - : uriLabel; - const sortText = description ? 'z' : '{'; // after `z` - - return { - label: { label: basename, description: labelDescription }, - filterText: `${folderLeader}${basename}`, - insertText: info.varWord?.endColumn === info.replace.endColumn ? `${text} ` : text, - range: info, - kind: CompletionItemKind.Folder, - sortText, - command: { - id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.folder', - prefix: 'folder', - isFile: false, - isDirectory: true, - range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, - data: resource - })] + const workspaces = this.workspaceContextService.getWorkspace().folders.map(folder => folder.uri); + + for (const workspace of workspaces) { + const { folders, files } = await searchFilesAndFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService); + for (const file of files) { + if (!seen.has(file)) { + result.suggestions.push(makeCompletionItem(file, FileKind.FILE)); + seen.add(file); + } } - }; - }; - - const seen = new ResourceSet(); - const workspaces = this.workspaceContextService.getWorkspace().folders.map(folder => folder.uri); - - let pattern: string | undefined; - if (info.varWord?.word && info.varWord.word.startsWith(folderLeader)) { - pattern = info.varWord.word.toLowerCase().slice(folderLeader.length); - - for (const folder of await getTopLevelFolders(workspaces, this.fileService)) { - result.suggestions.push(makeFolderCompletionItem(folder)); - seen.add(folder); - } - } - - // SEARCH - // use folder search when having a pattern - if (pattern) { - - const cacheKey = this.updateCacheKey(); - - const folders = await Promise.all(workspaces.map(workspace => searchFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService))); - for (const resource of folders.flat()) { - if (seen.has(resource)) { - // already included via history - continue; + for (const folder of folders) { + if (!seen.has(folder)) { + result.suggestions.push(makeCompletionItem(folder, FileKind.FOLDER)); + seen.add(folder); + } } - seen.add(resource); - result.suggestions.push(makeFolderCompletionItem(resource)); } } @@ -842,11 +733,11 @@ class BuiltinDynamicCompletions extends Disposable { sortText, command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.symbol', - prefix: 'sym', + id: `vscode.symbol/${JSON.stringify(symbolItem.location)}`, fullName: symbolItem.name, range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, - data: symbolItem.location + data: symbolItem.location, + icon: SymbolKinds.toIcon(symbolItem.kind) })] } }; @@ -859,29 +750,13 @@ class BuiltinDynamicCompletions extends Disposable { const symbolsToAdd: { symbol: DocumentSymbol; uri: URI }[] = []; for (const outlineModel of this.outlineService.getCachedModels()) { - if (pattern) { - symbolsToAdd.push(...outlineModel.asListOfDocumentSymbols().map(symbol => ({ symbol, uri: outlineModel.uri }))); - } else { - symbolsToAdd.push(...outlineModel.getTopLevelSymbols().map(symbol => ({ symbol, uri: outlineModel.uri }))); + const symbols = outlineModel.asListOfDocumentSymbols(); + for (const symbol of symbols) { + symbolsToAdd.push({ symbol, uri: outlineModel.uri }); } } - const symbolsToAddFiltered = symbolsToAdd.filter(fileSymbol => { - switch (fileSymbol.symbol.kind) { - case SymbolKind.Enum: - case SymbolKind.Class: - case SymbolKind.Method: - case SymbolKind.Function: - case SymbolKind.Namespace: - case SymbolKind.Module: - case SymbolKind.Interface: - return true; - default: - return false; - } - }); - - for (const symbol of symbolsToAddFiltered) { + for (const symbol of symbolsToAdd) { result.suggestions.push(makeSymbolCompletionItem({ ...symbol.symbol, location: { uri: symbol.uri, range: symbol.symbol.range } }, pattern ?? '')); } @@ -966,7 +841,6 @@ class ToolCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @ILanguageModelToolsService toolsService: ILanguageModelToolsService ) { super(); @@ -987,16 +861,24 @@ class ToolCompletions extends Disposable { const usedTools = widget.parsedInput.parts.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart); const usedToolNames = new Set(usedTools.map(v => v.toolName)); const toolItems: CompletionItem[] = []; - toolItems.push(...Array.from(toolsService.getTools()) + toolItems.push(...widget.input.selectedToolsModel.tools.get() .filter(t => t.canBeReferencedInPrompt) .filter(t => !usedToolNames.has(t.toolReferenceName ?? '')) .map((t): CompletionItem => { + const source = t.source; + const detail = source.type === 'mcp' + ? localize('desc', "MCP Server: {0}", source.label) + : source.type === 'extension' + ? source.label + : undefined; + const withLeader = `${chatVariableLeader}${t.toolReferenceName}`; return { label: withLeader, range, + detail, insertText: withLeader + ' ', - documentation: t.userDescription, + documentation: t.userDescription ?? t.modelDescription, kind: CompletionItemKind.Text, sortText: 'z' }; diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 06957dd285a..3f2bb1b4b84 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -4,16 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { themeColorFromId } from '../../../../../base/common/themables.js'; import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; +import { TrackedRangeStickiness } from '../../../../../editor/common/model.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { inputPlaceholderForeground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../common/chatColors.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget } from '../chatWidget.js'; @@ -46,8 +48,7 @@ class InputEditorDecorations extends Disposable { this.codeEditorService.registerDecorationType(decorationDescription, placeholderDecorationType, {}); - this._register(this.themeService.onDidColorThemeChange(() => this.updateRegisteredDecorationTypes())); - this.updateRegisteredDecorationTypes(); + this.registeredDecorationTypes(); this.updateInputEditorDecorations(); this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations())); @@ -73,28 +74,30 @@ class InputEditorDecorations extends Disposable { }); } - private updateRegisteredDecorationTypes() { - this.codeEditorService.removeDecorationType(variableTextDecorationType); - this.codeEditorService.removeDecorationType(dynamicVariableDecorationType); - this.codeEditorService.removeDecorationType(slashCommandTextDecorationType); + private registeredDecorationTypes() { - const theme = this.themeService.getColorTheme(); this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), borderRadius: '3px' }); this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), borderRadius: '3px' }); this.codeEditorService.registerDecorationType(decorationDescription, dynamicVariableDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), - borderRadius: '3px' + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), + borderRadius: '3px', + rangeBehavior: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }); - this.updateInputEditorDecorations(); + + this._register(toDisposable(() => { + this.codeEditorService.removeDecorationType(variableTextDecorationType); + this.codeEditorService.removeDecorationType(dynamicVariableDecorationType); + this.codeEditorService.removeDecorationType(slashCommandTextDecorationType); + })); } private getPlaceholderColor(): string | undefined { @@ -139,6 +142,7 @@ class InputEditorDecorations extends Disposable { const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); const slashCommandPart = parsedRequest.find((p): p is ChatRequestSlashCommandPart => p instanceof ChatRequestSlashCommandPart); + const slashPromptPart = parsedRequest.find((p): p is ChatRequestSlashPromptPart => p instanceof ChatRequestSlashPromptPart); const exactlyOneSpaceAfterPart = (part: IParsedChatRequestPart): boolean => { const partIdx = parsedRequest.indexOf(part); @@ -223,6 +227,10 @@ class InputEditorDecorations extends Disposable { textDecorations.push({ range: slashCommandPart.editorRange }); } + if (slashPromptPart) { + textDecorations.push({ range: slashPromptPart.editorRange }); + } + this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); const varDecorations: IDecorationOptions[] = []; @@ -304,7 +312,7 @@ class ChatTokenDeleter extends Disposable { const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentMode }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping - const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestToolPart); + const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestSlashPromptPart || p instanceof ChatRequestToolPart); deletableTokens.forEach(token => { const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range); // Part of this token was deleted, or the space after it was deleted, and the deletion range doesn't go off the front of the token, for simpler math diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 44c4d6d8946..5b1f9bd553c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -98,7 +98,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben disposableStore.add(onDebouncedType(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); - disposableStore.add(widget.attachmentModel.onDidChangeContext(() => { + disposableStore.add(widget.attachmentModel.onDidChange(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); disposableStore.add(currentEditingSession.onDidDispose(() => { diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts index ca08338bb43..f030477d0df 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/screenshot.ts +++ b/code/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), - isImage: true, + value: buffer.buffer, + kind: 'image' }; } diff --git a/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts index b5f426737fd..caa02352006 100644 --- a/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/code/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -3,6 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; /** * Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images. @@ -11,23 +16,24 @@ * @returns A promise that resolves to the UInt8Array string of the resized image. */ -export async function resizeImage(data: Uint8Array | string): Promise { +export async function resizeImage(data: Uint8Array | string, mimeType?: string): Promise { + const isGif = mimeType === 'image/gif'; if (typeof data === 'string') { data = convertStringToUInt8Array(data); } - const blob = new Blob([data]); - const img = new Image(); - const url = URL.createObjectURL(blob); - img.src = url; - return new Promise((resolve, reject) => { + const blob = new Blob([data], { type: mimeType }); + const img = new Image(); + const url = URL.createObjectURL(blob); + img.src = url; + img.onload = () => { URL.revokeObjectURL(url); let { width, height } = img; - if (width <= 768 || height <= 768) { + if ((width <= 768 || height <= 768) && !isGif) { resolve(data); return; } @@ -102,3 +108,51 @@ function isValidBase64(str: string): boolean { } })(); } + +export async function createFileForMedia(fileService: IFileService, imagesFolder: URI, dataTransfer: Uint8Array, mimeType: string): Promise { + const exists = await fileService.exists(imagesFolder); + if (!exists) { + await fileService.createFolder(imagesFolder); + } + + const ext = mimeType.split('/')[1] || 'png'; + const filename = `image-${Date.now()}.${ext}`; + const fileUri = joinPath(imagesFolder, filename); + + const buffer = VSBuffer.wrap(dataTransfer); + await fileService.writeFile(fileUri, buffer); + + return fileUri; +} + +export async function cleanupOldImages(fileService: IFileService, logService: ILogService, imagesFolder: URI): Promise { + const exists = await fileService.exists(imagesFolder); + if (!exists) { + return; + } + + const duration = 7 * 24 * 60 * 60 * 1000; // 7 days + const files = await fileService.resolve(imagesFolder); + if (!files.children) { + return; + } + + await Promise.all(files.children.map(async (file) => { + try { + const timestamp = getTimestampFromFilename(file.name); + if (timestamp && (Date.now() - timestamp > duration)) { + await fileService.del(file.resource); + } + } catch (err) { + logService.error('Failed to clean up old images', err); + } + })); +} + +function getTimestampFromFilename(filename: string): number | undefined { + const match = filename.match(/image-(\d+)\./); + if (match) { + return parseInt(match[1], 10); + } + return undefined; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 4c3465c0c8e..c9529b37743 100644 --- a/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { renderStringAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { assertNever } from '../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { encodeBase64 } from '../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -33,7 +31,8 @@ import { ChatModel } from '../common/chatModel.js'; import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js'; import { IChatService } from '../common/chatService.js'; import { ChatConfiguration } from '../common/constants.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, stringifyPromptTsxPart } from '../common/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart } from '../common/languageModelToolsService.js'; +import { getToolConfirmationAlert } from './chatAccessibilityProvider.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -42,6 +41,11 @@ interface IToolEntry { impl?: IToolImpl; } +interface ITrackedCall { + invocation?: ChatToolInvocation; + store: IDisposable; +} + export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -55,7 +59,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _toolContextKeys = new Set(); private readonly _ctxToolsCount: IContextKey; - private _callsByRequestId = new Map(); + private _callsByRequestId = new Map(); private _workspaceToolConfirmStore: Lazy; private _profileToolConfirmStore: Lazy; @@ -107,7 +111,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo let store: DisposableStore | undefined; if (toolData.inputSchema) { store = new DisposableStore(); - const schemaUrl = URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolData.id}` }).toString(); + const schemaUrl = createToolSchemaUri(toolData.id).toString(); jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store); store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`)); } @@ -237,7 +241,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (!this._callsByRequestId.has(requestId)) { this._callsByRequestId.set(requestId, []); } - this._callsByRequestId.get(requestId)!.push(store); + const trackedCall: ITrackedCall = { store }; + this._callsByRequestId.get(requestId)!.push(trackedCall); const source = new CancellationTokenSource(); store.add(toDisposable(() => { @@ -254,13 +259,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const prepared = await this.prepareToolInvocation(tool, dto, token); toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId); + trackedCall.invocation = toolInvocation; if (this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace)) { toolInvocation.confirmed.complete(true); } model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { - this._accessibilityService.alert(localize('toolConfirmationMessage', "Action required: {0}", prepared.confirmationMessages.title)); + this._accessibilityService.alert(this._instantiationService.invokeFunction(getToolConfirmationAlert, prepared.confirmationMessages.title)); const userConfirmed = await toolInvocation.confirmed.p; if (!userConfirmed) { throw new CancellationError(); @@ -287,7 +293,11 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo throw new CancellationError(); } - toolResult = await tool.impl.invoke(dto, countTokens, token); + toolResult = await tool.impl.invoke(dto, countTokens, { + report: step => { + toolInvocation?.acceptProgress(step); + } + }, token); this.ensureToolDetails(dto, toolResult, tool.data); this._telemetryService.publicLog2( @@ -311,7 +321,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined, toolSourceKind: tool.data.source.type, }); - this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId}: ${toErrorMessage(err)}. With parameters ${JSON.stringify(dto.parameters)}`); + this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`); + + toolResult ??= { content: [] }; + toolResult.toolResultError = err instanceof Error ? err.message : String(err); + if (tool.data.alwaysDisplayInputOutput) { + toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'text', value: String(err) }], isError: true }; + } + throw err; } finally { toolInvocation?.complete(toolResult); @@ -323,28 +340,10 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, token: CancellationToken): Promise { - let prepared = tool.impl!.prepareToolInvocation ? + const prepared = tool.impl!.prepareToolInvocation ? await tool.impl!.prepareToolInvocation(dto.parameters, token) : undefined; - if (!prepared?.confirmationMessages && tool.data.requiresConfirmation && tool.data.source.type === 'extension') { - if (!prepared) { - prepared = {}; - } - - const toolWarning = localize( - 'tool.warning', - "{0} This tool is from the extension `{1}`. Please carefully review any requested actions.", - '$(info)', - tool.data.source.extensionId.value, - ); - prepared.confirmationMessages = { - title: localize('msg.title', "Run {0}", `"${tool.data.displayName}"`), - message: new MarkdownString((tool.data.userDescription ?? tool.data.modelDescription) + '\n\n' + toolWarning, { supportThemeIcons: true }), - allowAutoConfirm: true, - }; - } - if (prepared?.confirmationMessages) { if (prepared.toolSpecificData?.kind !== 'terminal' && typeof prepared.confirmationMessages.allowAutoConfirm !== 'boolean') { prepared.confirmationMessages.allowAutoConfirm = true; @@ -364,22 +363,28 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void { if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) { toolResult.toolResultDetails = { - input: JSON.stringify(dto.parameters, undefined, 2), - output: this.toolResultToString(toolResult), + input: this.formatToolInput(dto), + output: this.toolResultToIO(toolResult), }; } } - private toolResultToString(toolResult: IToolResult): string { - const strs = []; - for (const part of toolResult.content) { + private formatToolInput(dto: IToolInvocation): string { + return JSON.stringify(dto.parameters, undefined, 2); + } + + private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] { + return toolResult.content.map(part => { if (part.kind === 'text') { - strs.push(part.value); + return { type: 'text', value: part.value }; } else if (part.kind === 'promptTsx') { - strs.push(stringifyPromptTsxPart(part)); + return { type: 'text', value: stringifyPromptTsxPart(part) }; + } else if (part.kind === 'data') { + return { type: 'data', value64: encodeBase64(part.value.data), mimeType: part.value.mimeType }; + } else { + assertNever(part); } - } - return strs.join(''); + }); } private shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined): boolean { @@ -405,7 +410,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private cleanupCallDisposables(requestId: string, store: DisposableStore): void { const disposables = this._callsByRequestId.get(requestId); if (disposables) { - const index = disposables.indexOf(store); + const index = disposables.findIndex(d => d.store === store); if (index > -1) { disposables.splice(index, 1); } @@ -419,7 +424,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo cancelToolCallsForRequest(requestId: string): void { const calls = this._callsByRequestId.get(requestId); if (calls) { - calls.forEach(call => call.dispose()); + calls.forEach(call => call.store.dispose()); this._callsByRequestId.delete(requestId); } } @@ -427,7 +432,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo public override dispose(): void { super.dispose(); - this._callsByRequestId.forEach(calls => dispose(calls)); + this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose())); this._ctxToolsCount.reset(); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chat.css b/code/src/vs/workbench/contrib/chat/browser/media/chat.css index ea0a6ea8303..44e238bd447 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -6,6 +6,7 @@ .interactive-session { max-width: 850px; margin: auto; + position: relative; /* For chat dnd */ } .interactive-list > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-tl-row > .monaco-tl-twistie { @@ -24,7 +25,7 @@ -webkit-user-select: text; } -.interactive-item-container .header { +.interactive-item-container:not(:has(.chat-extensions-content-part)) .header { display: flex; align-items: center; justify-content: space-between; @@ -200,8 +201,14 @@ margin-bottom: 8px; } -.interactive-item-container .value .rendered-markdown .codicon { - font-size: inherit; +.interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) { + .codicon { + font-size: inherit; + } + + .interactive-result-code-block .codicon { + font-size: initial; + } } .interactive-item-container .value .rendered-markdown blockquote { @@ -234,6 +241,10 @@ color: var(--vscode-textLink-foreground); } +.interactive-item-container .value .rendered-markdown .chat-extensions-content-part a { + color: inherit; +} + .interactive-item-container .value .rendered-markdown a { user-select: text; } @@ -306,13 +317,13 @@ font-weight: unset; } -.interactive-item-container .value .rendered-markdown { - /* Codicons next to text need to be aligned with the text */ - .codicon { - position: relative; - top: 2px; - } +/* Codicons next to text need to be aligned with the text */ +.interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) .codicon { + position: relative; + top: 2px; +} +.interactive-item-container .value .rendered-markdown { .chat-codeblock-pill-widget .codicon { top: -1px; } @@ -400,6 +411,10 @@ .tool-input-output-part.expanded .input-output { display: inherit; } + + &:not(:last-child) { + margin-bottom: 8px; + } } .interactive-item-container .value > .rendered-markdown li > p { @@ -541,7 +556,7 @@ have to be updated for changes to the rules above, or to support more deeply nes right: 0; } -.interactive-session .chat-dnd-overlay { +.chat-dnd-overlay { position: absolute; top: 0; left: 0; @@ -552,13 +567,13 @@ have to be updated for changes to the rules above, or to support more deeply nes display: none; } -.interactive-session .chat-dnd-overlay.visible { +.chat-dnd-overlay.visible { display: flex; align-items: center; justify-content: center; } -.interactive-session .chat-dnd-overlay .attach-context-overlay-text { +.chat-dnd-overlay .attach-context-overlay-text { padding: 0.6em; margin: 0.2em; line-height: 12px; @@ -568,7 +583,7 @@ have to be updated for changes to the rules above, or to support more deeply nes text-align: center; } -.interactive-session .chat-dnd-overlay .attach-context-overlay-text .codicon { +.chat-dnd-overlay .attach-context-overlay-text .codicon { height: 12px; font-size: 12px; margin-right: 3px; @@ -1037,7 +1052,8 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-notificationsWarningIcon-foreground); } -.chat-attached-context .chat-attached-context-attachment.show-file-icons.warning { +.chat-attached-context .chat-attached-context-attachment.show-file-icons.warning, +.chat-attached-context .chat-attached-context-attachment.show-file-icons.partial-warning { border-color: var(--vscode-notificationsWarningIcon-foreground); } @@ -1138,17 +1154,7 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 8px 0 0 0 } -.action-item.chat-attached-context-attachment.chat-add-files { - height: 20px; - color: var(--vscode-foreground); -} - -.action-item.chat-attached-context-attachment.chat-add-files span.keybinding { - display: none; -} - -.action-item.chat-mcp .action-label, -.action-item.chat-attached-context-attachment.chat-add-files .action-label, +.action-item.chat-attachment-button .action-label, .interactive-session .chat-attached-context .chat-attached-context-attachment { display: flex; gap: 2px; @@ -1162,10 +1168,9 @@ have to be updated for changes to the rules above, or to support more deeply nes width: fit-content; } -.action-item.chat-attached-context-attachment.chat-add-files .action-label { - color: var(--vscode-foreground); - font-family: unset; - gap: 5px; +.action-item.chat-attachment-button > .action-label > .codicon { + font-size: 14px; + height: auto; } .action-item.chat-mcp { @@ -1239,8 +1244,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { - display: block !important; - align-items: center !important; + display: inline-flex; + align-items: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -1253,8 +1258,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { - padding-left: 4px; - font-size: 100% !important; + font-size: 14px; } .interactive-session .chat-input-container .chat-attached-context { @@ -1325,10 +1329,9 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label::before { - height: 1em; - width: auto; + height: auto; padding: 0; - line-height: 1em !important; + line-height: 100% !important; align-self: center; background-size: contain; @@ -1676,7 +1679,8 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 0 3px; } -.interactive-item-container .chat-confirmation-widget .interactive-result-code-block { +.interactive-item-container .chat-confirmation-widget .interactive-result-code-block, +.interactive-item-container .chat-confirmation-widget .chat-attached-context { margin-bottom: 8px; } @@ -1701,14 +1705,50 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-textLink-foreground); } -.chat-attached-context-hover .chat-attached-context-image { +.chat-attached-context-hover { + padding: 0 6px; +} + +.chat-attached-context-hover .chat-attached-context-image-container { + padding: 6px 0 4px; height: auto; - max-height: 512px; - max-width: 512px; width: 100%; display: block; } +.chat-attached-context-hover .chat-attached-context-image-container .chat-attached-context-image { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + max-height: 350px; + max-width: 100%; + min-width: 200px; + min-height: 200px; + +} + +.chat-attached-context-hover .chat-attached-context-url { + color: var(--vscode-textLink-foreground); + cursor: pointer; + margin-top: 4px; + padding: 2px 0; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + display: block; +} + +.chat-attached-context-hover .chat-attached-context-url-separator { + border-top: 1px solid var(--vscode-chat-requestBorder); + left: 0; + right: 0; + position: absolute; + margin-top: 2px; +} + .chat-attached-context-attachment .chat-attached-context-pill { font-size: 12px; display: inline-flex; @@ -1730,6 +1770,7 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 14px; height: 14px; border-radius: 2px; + object-fit: cover; } .chat-attached-context-attachment .chat-attached-context-custom-text { @@ -1749,6 +1790,10 @@ have to be updated for changes to the rules above, or to support more deeply nes text-decoration: line-through; } +.chat-attached-context-attachment.show-file-icons.partial-warning .chat-attached-context-custom-text { + color: var(--vscode-notificationsWarningIcon-foreground); +} + .interactive-session .chat-scroll-down { display: none; position: absolute; diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css b/code/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css index 9356b690797..462d898b3d3 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css @@ -35,40 +35,22 @@ .chat-editor-overlay-widget .chat-editor-overlay-progress { align-items: center; display: none; - padding: 0px 5px; + padding: 5px 0 5px 5px; font-size: 12px; - font-variant-numeric: tabular-nums; overflow: hidden; - white-space: nowrap; + gap: 6px; } .chat-editor-overlay-widget.busy .chat-editor-overlay-progress { display: inline-flex; } - -@keyframes ellipsis { - 0% { - content: ""; - } - 25% { - content: "."; - } - 50% { - content: ".."; - } - 75% { - content: "..."; - } - 100% { - content: ""; - } -} - -.chat-editor-overlay-widget.busy.paused .chat-editor-overlay-progress { - .codicon-loading { - display: none; - } +.chat-editor-overlay-widget .chat-editor-overlay-progress .progress-message { + white-space: nowrap; + max-width: 13em; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 8px; } .chat-editor-overlay-widget .action-item > .action-label { @@ -76,14 +58,6 @@ font-size: 12px; } -.chat-editor-overlay-widget.busy .action-item > .action-label.busy::after { - content: ""; - display: inline-flex; - white-space: nowrap; - overflow: hidden; - width: 3ch; - animation: ellipsis steps(4, end) 1s infinite; -} .chat-editor-overlay-widget .action-item:first-child > .action-label { padding-left: 7px; diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css b/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css deleted file mode 100644 index 79c7251217f..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css +++ /dev/null @@ -1,146 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-editor-overlay-widget { - padding: 0px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - border-radius: 5px; - border: 1px solid var(--vscode-contrastBorder); - display: flex; - align-items: center; - z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); - overflow: hidden; -} - -@keyframes pulse { - 0% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); - } - 50% { - box-shadow: 0 2px 8px 4px var(--vscode-widget-shadow); - } - 100% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); - } -} - -.chat-editor-overlay-widget.busy { - animation: pulse ease-in 2.3s infinite; -} - -.chat-editor-overlay-widget .chat-editor-overlay-progress { - align-items: center; - display: none; - padding: 0px 5px; - font-size: 12px; - font-variant-numeric: tabular-nums; - overflow: hidden; - white-space: nowrap; -} - -.chat-editor-overlay-widget.busy .chat-editor-overlay-progress { - display: inline-flex; -} - -.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .busy-label { - padding: 5px; - /* font-style: italic; */ -} - -@keyframes ellipsis { - 0% { - content: ""; - } - 25% { - content: "."; - } - 50% { - content: ".."; - } - 75% { - content: "..."; - } - 100% { - content: ""; - } -} - -.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .busy-label::after { - content: ""; - display: inline-flex; - white-space: nowrap; - overflow: hidden; - width: 3ch; - animation: ellipsis steps(4, end) 1s infinite; -} - -.chat-editor-overlay-widget.busy.paused .chat-editor-overlay-progress { - .codicon-loading { - display: none; - } - - .busy-label::after { - animation-duration: 5s; - } -} - -.chat-editor-overlay-widget.busy .chat-editor-overlay-toolbar { - display: none; -} - -.chat-editor-overlay-widget .action-item > .action-label { - padding: 5px; - font-size: 12px; -} - -.chat-editor-overlay-widget .action-item:first-child > .action-label { - padding-left: 7px; -} - -.chat-editor-overlay-widget .action-item:last-child > .action-label { - padding-right: 7px; -} - -.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .codicon, -.chat-editor-overlay-widget .action-item > .action-label.codicon { - color: var(--vscode-button-foreground); -} - -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label.codicon::before, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label.codicon, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label:hover { - color: var(--vscode-button-foreground); - opacity: 0.7; -} - - -.chat-editor-overlay-widget .action-item.label-item { - font-variant-numeric: tabular-nums; -} - -.chat-editor-overlay-widget .monaco-action-bar .action-item.label-item > .action-label, -.chat-editor-overlay-widget .monaco-action-bar .action-item.label-item > .action-label:hover { - color: var(--vscode-button-foreground); - opacity: 1; -} - -.chat-editor-overlay-widget .action-item.auto { - position: relative; - overflow: hidden; -} - -.chat-editor-overlay-widget .action-item.auto::before { - content: ''; - position: absolute; - top: 0; - left: var(--vscode-action-item-auto-timeout, -100%); - width: 100%; - height: 100%; - background-color: var(--vscode-toolbar-hoverBackground); - transition: left 0.5s linear; -} diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatSetup.css b/code/src/vs/workbench/contrib/chat/browser/media/chatSetup.css index b042970d0d8..3d1e37d5e2f 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatSetup.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chatSetup.css @@ -3,25 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.chat-welcome-view .chat-setup-view { - - text-align: center; - - .chat-features-container { - display: flex; - justify-content: center; - text-align: initial; - border-radius: 2px; - border: 1px solid var(--vscode-chat-requestBorder); - background-color: var(--vscode-chat-requestBackground); - } -} - -.dialog-message-body .chat-setup-view { +.chat-setup-dialog { p { margin-top: 0; margin-bottom: 0; + width: 100%; } p.setup-legal { @@ -35,14 +22,6 @@ color: var(--vscode-descriptionForeground); margin-top: 1em; } -} - -.dialog-message-body .chat-setup-view, -.chat-welcome-view .chat-setup-view { - - p { - width: 100%; - } .chat-feature-container { display: flex; diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/code/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index a8fe69420ba..ee1f9c7c300 100644 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/code/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -16,12 +16,19 @@ } .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 { + font-size: 11px; color: var(--vscode-descriptionForeground); } @@ -48,15 +55,21 @@ .chat-status-bar-entry-tooltip .quota-indicator .quota-label { display: flex; justify-content: space-between; + gap: 20px; margin-bottom: 3px; } +.chat-status-bar-entry-tooltip .quota-indicator .quota-label .quota-value { + color: var(--vscode-descriptionForeground); +} + .chat-status-bar-entry-tooltip .quota-indicator .quota-bar { width: 100%; height: 4px; background-color: var(--vscode-gauge-foreground); border-radius: 4px; border: 1px solid var(--vscode-gauge-border); + margin: 4px 0; } .chat-status-bar-entry-tooltip .quota-indicator .quota-bar .quota-bit { @@ -100,10 +113,6 @@ margin-right: 5px; } -.chat-status-bar-entry-tooltip .settings .setting .codicon { - font-size: 12px; -} - .chat-status-bar-entry-tooltip .settings .setting .setting-label { cursor: pointer; } @@ -117,9 +126,18 @@ .chat-status-bar-entry-tooltip .contribution .body { display: flex; flex-direction: row; - gap: 6px; + align-items: center; + gap: 5px; + .description, .detail-item { + display: flex; + align-items: center; + gap: 3px; + } + + .detail-item, + .detail-item a { margin-left: auto; color: var(--vscode-descriptionForeground); } diff --git a/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css deleted file mode 100644 index 785aeaa2fa7..00000000000 --- a/code/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-welcome-view .chat-setup-view { - text-align: center; - - p { - width: 100%; - } - - .chat-features-container { - display: flex; - justify-content: center; - text-align: initial; - border-radius: 2px; - border: 1px solid var(--vscode-chat-requestBorder); - background-color: var(--vscode-chat-requestBackground); - } - - .chat-feature-container { - display: flex; - align-items: center; - gap: 6px; - padding: 5px 10px 5px 10px; - } - - .chat-feature-container .codicon[class*='codicon-'] { - font-size: 16px; - } - - .codicon[class*='codicon-'] { - font-size: 13px; - line-height: 1.4em; - vertical-align: bottom; - } - - .button-container { - padding-top: 20px; - } - - /** Dropdown Button */ - .monaco-button-dropdown { - width: 100%; - } - - .monaco-button-dropdown .monaco-text-button { - width: 100%; - } -} diff --git a/code/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css b/code/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css new file mode 100644 index 00000000000..ad5d4e5f4d2 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.element-selection-message, +.element-expand-container { + position: absolute; + bottom: 10px; + right: 10px; + padding: 0px 10px; + background: var(--vscode-notifications-background); + color: var(--vscode-notifications-foreground); + border-radius: 4px; + font-size: 12px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + gap: 8px; + width: max-content; + z-index: 1; + height: 42px; +} + +.element-selection-message { + bottom: 10px; + right: 10px; +} + +.element-expand-container { + bottom: 15px; + right: 15px; +} + +.element-selection-cancel, +.element-selection-start { + padding: 2px 8px; + width: fit-content; +} + +.element-selection-message .monaco-button.codicon.codicon-close, +.element-expand-container .monaco-button.codicon.codicon-layout, +.element-selection-message .monaco-button.codicon.codicon-chevron-right, +.element-selection-message .monaco-button.codicon.codicon-gear { + width: 17px; + height: 17px; + padding: 2px 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--vscode-descriptionForeground); + border: none; + outline: none; + padding: 0; + border-radius: 5px; + cursor: pointer; +} + +.element-selection .monaco-button { + height: 17px; + width: fit-content; + padding: 2px 6px; + font-size: 11px; + background-color: var(--vscode-button-background); + border: 1px solid var(--vscode-button-border); + color: var(--vscode-button-foreground); +} + +.element-selection-message .monaco-button:hover, +.element-expand-container .monaco-button:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.element-selection-message .hidden, +.element-expand-container.hidden, +.element-selection-message.hidden { + display: none !important; +} diff --git a/code/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts b/code/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts new file mode 100644 index 00000000000..b5d6167a2a9 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction } from '../../../../../base/common/actions.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { MenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { IChatAgentService } from '../../common/chatAgents.js'; +import { ChatMode, modeToString } from '../../common/constants.js'; +import { getOpenChatActionIdForMode } from '../actions/chatActions.js'; +import { IToggleChatModeArgs } from '../actions/chatExecuteActions.js'; + +export interface IModePickerDelegate { + onDidChangeMode: Event; + getMode(): ChatMode; +} + +export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { + constructor( + action: MenuItemAction, + private readonly delegate: IModePickerDelegate, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IChatAgentService chatAgentService: IChatAgentService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + const makeAction = (mode: ChatMode): IAction => ({ + ...action, + id: getOpenChatActionIdForMode(mode), + label: modeToString(mode), + class: undefined, + enabled: true, + checked: delegate.getMode() === mode, + run: async () => { + const result = await action.run({ mode } satisfies IToggleChatModeArgs); + this.renderLabel(this.element!); + return result; + } + }); + + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const agentStateActions = [ + makeAction(ChatMode.Edit), + ]; + if (chatAgentService.hasToolsAgent) { + agentStateActions.push(makeAction(ChatMode.Agent)); + } + + agentStateActions.unshift(makeAction(ChatMode.Ask)); + return agentStateActions; + } + }; + + const modelPickerActionWidgetOptions: Omit = { + actionProvider, + showItemKeybindings: false + }; + + super(action, modelPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService); + + this._register(delegate.onDidChangeMode(() => this.renderLabel(this.element!))); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + if (!this.element) { + return null; + } + this.setAriaLabelAttributes(element); + const state = modeToString(this.delegate.getMode()); + dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); + return null; + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-modelPicker-item'); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/code/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts new file mode 100644 index 00000000000..4096e758f80 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from '../../../../../base/common/actions.js'; +import { Event } from '../../../../../base/common/event.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; +import { localize } from '../../../../../nls.js'; +import * as dom from '../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; + +export interface IModelPickerDelegate { + readonly onDidChangeModel: Event; + getCurrentModel(): ILanguageModelChatMetadataAndIdentifier | undefined; + setModel(model: ILanguageModelChatMetadataAndIdentifier): void; + getModels(): ILanguageModelChatMetadataAndIdentifier[]; +} + +function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate): IActionWidgetDropdownActionProvider { + return { + getActions: () => { + return delegate.getModels().map(model => { + return { + id: model.metadata.id, + enabled: true, + checked: model.metadata.id === delegate.getCurrentModel()?.metadata.id, + category: model.metadata.modelPickerCategory, + class: undefined, + description: model.metadata.cost, + tooltip: model.metadata.description ?? model.metadata.name, + label: model.metadata.name, + run: () => { + delegate.setModel(model); + } + } satisfies IActionWidgetDropdownAction; + }); + } + }; +} + +function getModelPickerActionBarActions(menuService: IMenuService, contextKeyService: IContextKeyService, commandService: ICommandService, chatEntitlementService: IChatEntitlementService): IAction[] { + const menuActions = menuService.createMenu(MenuId.ChatModelPicker, contextKeyService); + const menuContributions = getFlatActionBarActions(menuActions.getActions()); + menuActions.dispose(); + + const additionalActions: IAction[] = []; + + // Add menu contributions from extensions + if (menuContributions.length > 0) { + additionalActions.push(...menuContributions); + } + + // Add upgrade option if entitlement is limited + if (chatEntitlementService.entitlement === ChatEntitlement.Limited) { + additionalActions.push({ + id: 'moreModels', + label: localize('chat.moreModels', "Add Premium Models"), + enabled: true, + tooltip: localize('chat.moreModels.tooltip', "Add premium models"), + class: undefined, + run: () => { + const commandId = 'workbench.action.chat.upgradePlan'; + commandService.executeCommand(commandId); + } + }); + } + + return additionalActions; +} + +/** + * Action view item for selecting a language model in the chat interface. + */ +export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem { + constructor( + action: IAction, + private currentModel: ILanguageModelChatMetadataAndIdentifier, + delegate: IModelPickerDelegate, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IKeybindingService keybindingService: IKeybindingService, + ) { + // Modify the original action with a different label and make it show the current model + const actionWithLabel: IAction = { + ...action, + label: currentModel.metadata.name, + tooltip: localize('chat.modelPicker.label', "Pick Model"), + run: () => { } + }; + + const modelPickerActionWidgetOptions: Omit = { + actionProvider: modelDelegateToWidgetActionsProvider(delegate), + actionBarActions: getModelPickerActionBarActions(menuService, contextKeyService, commandService, chatEntitlementService) + }; + + super(actionWithLabel, modelPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService); + + // Listen for model changes from the delegate + this._register(delegate.onDidChangeModel(model => { + this.currentModel = model; + if (this.element) { + this.renderLabel(this.element); + } + })); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + dom.reset(element, dom.$('span.chat-model-label', undefined, this.currentModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); + return null; + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-modelPicker-item'); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts similarity index 61% rename from code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts rename to code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts index caacc52bd77..d6a8b99f892 100644 --- a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts +++ b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts @@ -9,31 +9,30 @@ import { CHAT_CATEGORY } from '../../actions/chatActions.js'; import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { ChatContextKeys } from '../../../common/chatContextKeys.js'; import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { runAttachInstructionsAction } from '../../actions/promptActions/index.js'; import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MenuId, MenuRegistry } from '../../../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IChatAttachPromptActionOptions, ATTACH_PROMPT_ACTION_ID } from '../../actions/chatAttachPromptAction/chatAttachPromptAction.js'; /** - * Command ID of the "Use Prompt" command. + * Command ID of the "Attach Instructions" command. */ -export const COMMAND_ID = 'workbench.command.prompts.use'; +export const INSTRUCTIONS_COMMAND_ID = 'workbench.command.instructions.attach'; /** - * Keybinding of the "Use Prompt" command. + * Keybinding of the "Use Instructions" command. * The `cmd + /` is the current keybinding for 'attachment', so we use - * the `alt` key modifier to convey the "prompt attachment" action. + * the `alt` key modifier to convey the "instructions attachment" action. */ -const COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; +const INSTRUCTIONS_COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; /** - * Implementation of the "Use Prompt" command. The command works in the following way. + * Implementation of the "Use Instructions" command. The command works in the following way. * * When executed, it tries to see if a `prompt file` was open in the active code editor * (see {@link IChatAttachPromptActionOptions.resource resource}), and if a chat input @@ -55,16 +54,14 @@ const command = async ( ): Promise => { const commandService = accessor.get(ICommandService); - const options: IChatAttachPromptActionOptions = { - resource: getActivePromptUri(accessor), + await runAttachInstructionsAction(commandService, { + resource: getActiveInstructionsFileUri(accessor), widget: getFocusedChatWidget(accessor), - }; - - await commandService.executeCommand(ATTACH_PROMPT_ACTION_ID, options); + }); }; /** - * Get chat widget reference to attach prompt to. + * Get chat widget reference to attach instructions to. */ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined { const chatWidgetService = accessor.get(IChatWidgetService); @@ -83,65 +80,37 @@ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | } /** - * Gets active editor instance, if any. - */ -export function getActiveCodeEditor(accessor: ServicesAccessor): IActiveCodeEditor | undefined { - const editorService = accessor.get(IEditorService); - const { activeTextEditorControl } = editorService; - - if (isCodeEditor(activeTextEditorControl) && activeTextEditorControl.hasModel()) { - return activeTextEditorControl; - } - - if (isDiffEditor(activeTextEditorControl)) { - const originalEditor = activeTextEditorControl.getOriginalEditor(); - if (!originalEditor.hasModel()) { - return undefined; - } - - return originalEditor; - } - - return undefined; -} - -/** - * Gets `URI` of a prompt file open in an active editor instance, if any. + * Gets `URI` of a instructions file open in an active editor instance, if any. */ -const getActivePromptUri = ( +export const getActiveInstructionsFileUri = ( accessor: ServicesAccessor, ): URI | undefined => { - const activeEditor = getActiveCodeEditor(accessor); - if (!activeEditor) { - return undefined; - } - - const { uri } = activeEditor.getModel(); - if (isPromptFile(uri)) { - return uri; + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) { + return model.uri; } - return undefined; }; /** - * Register the "Use Prompt" command with its keybinding. + * Register the "Attach Instructions" command with its keybinding. */ KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: COMMAND_ID, + id: INSTRUCTIONS_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, - primary: COMMAND_KEY_BINDING, + primary: INSTRUCTIONS_COMMAND_KEY_BINDING, handler: command, when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), }); /** - * Register the "Use Prompt" command in the `command palette`. + * Register the "Use Instructions" command in the `command palette`. */ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { - id: COMMAND_ID, - title: localize('commands.prompts.use.title', "Use Prompt"), + id: INSTRUCTIONS_COMMAND_ID, + title: localize('attach-instructions.capitalized.ellipses', "Attach Instructions..."), category: CHAT_CATEGORY }, when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) diff --git a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index 2cde08cb946..9908f01032f 100644 --- a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -3,61 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isEqual } from '../../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { getCodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; +import { SnippetController2 } from '../../../../../../../editor/contrib/snippet/browser/snippetController2.js'; import { localize } from '../../../../../../../nls.js'; -import { createPromptFile } from './utils/createPromptFile.js'; -import { CHAT_CATEGORY } from '../../../actions/chatActions.js'; -import { askForPromptName } from './dialogs/askForPromptName.js'; -import { ChatContextKeys } from '../../../../common/chatContextKeys.js'; -import { ILogService } from '../../../../../../../platform/log/common/log.js'; -import { askForPromptSourceFolder } from './dialogs/askForPromptSourceFolder.js'; +import { MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { ILogService } from '../../../../../../../platform/log/common/log.js'; +import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js'; import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; -import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; -import { ContextKeyExpr } from '../../../../../../../platform/contextkey/common/contextkey.js'; -import { MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js'; -import { IPromptPath, IPromptsService } from '../../../../common/promptSyntax/service/types.js'; import { IQuickInputService } from '../../../../../../../platform/quickinput/common/quickInput.js'; -import { ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IUserDataSyncEnablementService, SyncResource } from '../../../../../../../platform/userDataSync/common/userDataSync.js'; import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; +import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; import { CONFIGURE_SYNC_COMMAND_ID } from '../../../../../../services/userDataSync/common/userDataSync.js'; -import { IUserDataSyncEnablementService, SyncResource } from '../../../../../../../platform/userDataSync/common/userDataSync.js'; -import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../../../platform/notification/common/notification.js'; - -/** - * Base command ID prefix. - */ -const BASE_COMMAND_ID = 'workbench.command.prompts.create'; - -/** - * Command ID for creating a 'local' prompt. - */ -const LOCAL_COMMAND_ID = `${BASE_COMMAND_ID}.local`; - -/** - * Command ID for creating a 'user' prompt. - */ -const USER_COMMAND_ID = `${BASE_COMMAND_ID}.user`; - -/** - * Title of the 'create local prompt' command. - */ -const LOCAL_COMMAND_TITLE = localize('commands.prompts.create.title.local', "Create Prompt"); - -/** - * Title of the 'create user prompt' command. - */ -const USER_COMMAND_TITLE = localize('commands.prompts.create.title.user', "Create User Prompt"); +import { ISnippetsService } from '../../../../../snippets/browser/snippets.js'; +import { ChatContextKeys } from '../../../../common/chatContextKeys.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../../common/promptSyntax/constants.js'; +import { IPromptsService, TPromptsType } from '../../../../common/promptSyntax/service/types.js'; +import { CHAT_CATEGORY } from '../../../actions/chatActions.js'; +import { askForPromptFileName } from './dialogs/askForPromptName.js'; +import { askForPromptSourceFolder } from './dialogs/askForPromptSourceFolder.js'; +import { createPromptFile } from './utils/createPromptFile.js'; /** * The command implementation. */ const command = async ( accessor: ServicesAccessor, - type: IPromptPath['type'], + type: TPromptsType, ): Promise => { + const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const labelService = accessor.get(ILabelService); @@ -68,14 +51,23 @@ const command = async ( const notificationService = accessor.get(INotificationService); const workspaceService = accessor.get(IWorkspaceContextService); const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService); + const snippetService = accessor.get(ISnippetsService); + const editorService = accessor.get(IEditorService); - const fileName = await askForPromptName(type, quickInputService); - if (!fileName) { - return; - } + + const placeHolder = (type === 'instructions') + ? localize( + 'workbench.command.instructions.create.location.placeholder', + "Select a location to create the instructions file in...", + ) + : localize( + 'workbench.command.prompt.create.location.placeholder', + "Select a location to create the prompt file in...", + ); const selectedFolder = await askForPromptSourceFolder({ - type: type, + type, + placeHolder, labelService, openerService, promptsService, @@ -87,21 +79,35 @@ const command = async ( return; } - const content = localize( - 'workbench.command.prompts.create.initial-content', - "Add prompt contents...", - ); + const fileName = await askForPromptFileName(type, selectedFolder.uri, quickInputService, fileService); + if (!fileName) { + return; + } + const promptUri = await createPromptFile({ fileName, - folder: selectedFolder, - content, + folder: selectedFolder.uri, + content: '', fileService, openerService, }); await openerService.open(promptUri); - if (type !== 'user') { + const editor = getCodeEditor(editorService.activeTextEditorControl); + if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) { + const languageId = type === 'instructions' ? INSTRUCTIONS_LANGUAGE_ID : PROMPT_LANGUAGE_ID; + + const snippets = await snippetService.getSnippets(languageId, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + if (snippets.length > 0) { + SnippetController2.get(editor)?.apply([{ + range: editor.getModel().getFullModelRange(), + template: snippets[0].body + }]); + } + } + + if (selectedFolder.storage !== 'user') { return; } @@ -121,12 +127,12 @@ const command = async ( return; } - // show suggestion to enable synchronization of the user prompts to the user + // show suggestion to enable synchronization of the user prompts and instructions to the user notificationService.prompt( Severity.Info, localize( 'workbench.command.prompts.create.user.enable-sync-notification', - "User prompts are not currently synchronized. Do you want to enable synchronization of the user prompts?", + "Do you want to backup and sync your user prompt and instruction files with Setting Sync?'", ), [ { @@ -137,7 +143,13 @@ const command = async ( logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`); }); }, - } + }, + { + label: localize('learnMore.capitalized', "Learn More"), + run: () => { + openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); + }, + }, ], { neverShowAgain: { @@ -148,55 +160,43 @@ const command = async ( ); }; -/** - * Factory for creating the command handler with specific prompt `type`. - */ -const commandFactory = (type: 'local' | 'user') => { - return async (accessor: ServicesAccessor): Promise => { - return command(accessor, type); - }; -}; - -/** - * Register the "Create Prompt" command. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: LOCAL_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - handler: commandFactory('local'), - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), -}); - -/** - * Register the "Create User Prompt" command. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: USER_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - handler: commandFactory('user'), - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), -}); +function register(type: TPromptsType, id: string, title: string) { + /** + * Register the command. + */ + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id, + weight: KeybindingWeight.WorkbenchContrib, + handler: async (accessor: ServicesAccessor): Promise => { + return command(accessor, type); + }, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + }); -/** - * Register the "Create Prompt" command in the command palette. - */ -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: LOCAL_COMMAND_ID, - title: LOCAL_COMMAND_TITLE, - category: CHAT_CATEGORY - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) -}); + /** + * Register the command in the command palette. + */ + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id, + title, + category: CHAT_CATEGORY + }, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) + }); +} + +export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt'; +export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions'; + +register( + 'instructions', + NEW_INSTRUCTIONS_COMMAND_ID, + localize('commands.new.instructions.local.title', "New Instructions File...") +); +register( + 'prompt', + NEW_PROMPT_COMMAND_ID, + localize('commands.new.prompt.local.title', "New Prompt File...") +); -/** - * Register the "Create User Prompt" command in the command palette. - */ -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: USER_COMMAND_ID, - title: USER_COMMAND_TITLE, - category: CHAT_CATEGORY, - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) -}); diff --git a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index cd412256291..2ac54236cf9 100644 --- a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -4,37 +4,71 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../../../nls.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; +import { TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; +import { getPromptFileExtension } from '../../../../../../../../platform/prompts/common/constants.js'; import { IQuickInputService } from '../../../../../../../../platform/quickinput/common/quickInput.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import Severity from '../../../../../../../../base/common/severity.js'; +import { isValidBasename } from '../../../../../../../../base/common/extpath.js'; /** - * Asks the user for a prompt name. + * Asks the user for a file name. */ -export const askForPromptName = async ( - _type: 'local' | 'user', +export const askForPromptFileName = async ( + type: TPromptsType, + selectedFolder: URI, quickInputService: IQuickInputService, + fileService: IFileService, ): Promise => { - const result = await quickInputService.input( - { - placeHolder: localize( - 'commands.prompts.create.ask-name.placeholder', - "Provide a prompt name", - PROMPT_FILE_EXTENSION, - ), - }); + const placeHolder = (type === 'instructions') + ? localize('askForInstructionsFileName.placeholder', "Enter the name of the instructions file") + : localize('askForPromptFileName.placeholder', "Enter the name of the prompt file"); + + + const sanitizeInput = (input: string) => { + const trimmedName = input.trim(); + if (!trimmedName) { + return undefined; + } + + const fileExtension = getPromptFileExtension(type); + return (trimmedName.endsWith(fileExtension)) + ? trimmedName + : `${trimmedName}${fileExtension}`; + }; + + const validateInput = async (value: string) => { + const fileName = sanitizeInput(value); + if (!fileName) { + return { + content: localize('askForPromptFileName.error.empty', "Please enter a name."), + severity: Severity.Warning + }; + } + + if (!isValidBasename(fileName)) { + return { + content: localize('askForPromptFileName.error.invalid', "The name contains invalid characters."), + severity: Severity.Error + }; + } + + const fileUri = URI.joinPath(selectedFolder, fileName); + if (await fileService.exists(fileUri)) { + return { + content: localize('askForPromptFileName.error.exists', "A file for the given name already exists."), + severity: Severity.Error + }; + } - if (!result) { return undefined; - } + }; - const trimmedName = result.trim(); - if (!trimmedName) { + const result = await quickInputService.input({ placeHolder, validateInput }); + if (!result) { return undefined; } - const cleanName = (trimmedName.endsWith(PROMPT_FILE_EXTENSION)) - ? trimmedName - : `${trimmedName}${PROMPT_FILE_EXTENSION}`; - - return cleanName; + return sanitizeInput(result); }; diff --git a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts index 2f446a81472..bf71ed3e107 100644 --- a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts +++ b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts @@ -6,22 +6,21 @@ import { localize } from '../../../../../../../../nls.js'; import { URI } from '../../../../../../../../base/common/uri.js'; import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; import { basename, extUri } from '../../../../../../../../base/common/resources.js'; -import { IPromptsService } from '../../../../../common/promptSyntax/service/types.js'; import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; +import { PROMPT_DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; import { IWorkspaceContextService } from '../../../../../../../../platform/workspace/common/workspace.js'; +import { IPromptPath, IPromptsService, TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; /** * Options for {@link askForPromptSourceFolder} dialog. */ interface IAskForFolderOptions { - /** - * Prompt type. - */ - readonly type: 'local' | 'user'; + + readonly type: TPromptsType; + readonly placeHolder: string; readonly labelService: ILabelService; readonly openerService: IOpenerService; @@ -30,14 +29,18 @@ interface IAskForFolderOptions { readonly workspaceService: IWorkspaceContextService; } +interface IFolderQuickPickItem extends IQuickPickItem { + readonly folder: IPromptPath; +} + /** * Asks the user for a specific prompt folder, if multiple folders provided. * Returns immediately if only one folder available. */ export const askForPromptSourceFolder = async ( options: IAskForFolderOptions, -): Promise => { - const { type, promptsService, quickInputService, labelService, openerService, workspaceService } = options; +): Promise => { + const { type, placeHolder, promptsService, quickInputService, labelService, openerService, workspaceService } = options; // get prompts source folders based on the prompt type const folders = promptsService.getSourceFolders(type); @@ -52,20 +55,31 @@ export const askForPromptSourceFolder = async ( // if there is only one folder, no need to ask // note! when we add more actions to the dialog, this will have to go if (folders.length === 1) { - return folders[0].uri; + return folders[0]; } - const pickOptions: IPickOptions> = { - placeHolder: localize( - 'commands.prompts.create.ask-folder.placeholder', - "Select a prompt source folder", - ), + const pickOptions: IPickOptions = { + placeHolder, canPickMany: false, matchOnDescription: true, }; // create list of source folder locations - const foldersList = folders.map(({ uri }): WithUriValue => { + const foldersList = folders.map(folder => { + const uri = folder.uri; + if (folder.storage === 'user') { + return { + type: 'item', + label: localize( + 'commands.prompts.create.source-folder.user', + "User Data Folder", + ), + description: labelService.getUriLabel(uri), + tooltip: uri.fsPath, + folder + }; + } + const { folders } = workspaceService.getWorkspace(); const isMultirootWorkspace = (folders.length > 1); @@ -79,7 +93,7 @@ export const askForPromptSourceFolder = async ( label: basename(uri), description: labelService.getUriLabel(uri, { relative: true }), tooltip: uri.fsPath, - value: uri, + folder, }; } @@ -94,7 +108,7 @@ export const askForPromptSourceFolder = async ( // use absolute path as the description description: labelService.getUriLabel(uri, { relative: false }), tooltip: uri.fsPath, - value: uri, + folder, }; }); @@ -103,7 +117,7 @@ export const askForPromptSourceFolder = async ( return; } - return answer.value; + return answer.folder; }; /** @@ -122,9 +136,9 @@ const showNoFoldersDialog = async ( 'commands.prompts.create.ask-folder.empty.docs-label', 'Learn how to configure reusable prompts', ), - description: DOCUMENTATION_URL, - tooltip: DOCUMENTATION_URL, - value: URI.parse(DOCUMENTATION_URL), + description: PROMPT_DOCUMENTATION_URL, + tooltip: PROMPT_DOCUMENTATION_URL, + value: URI.parse(PROMPT_DOCUMENTATION_URL), }; const result = await quickInputService.pick( diff --git a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts index 4027531b5dc..dbea375a77d 100644 --- a/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts +++ b/code/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts @@ -10,7 +10,7 @@ import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; import { dirname } from '../../../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; -import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; +import { isPromptOrInstructionsFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; /** * Options for the {@link createPromptFile} utility. @@ -52,7 +52,7 @@ export const createPromptFile = async ( const promptUri = URI.joinPath(folder, fileName); assert( - isPromptFile(promptUri), + isPromptOrInstructionsFile(promptUri), new InvalidPromptName(fileName), ); diff --git a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 286f01f3b62..efd3a6b87e6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -10,7 +10,7 @@ import { Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { IMarkdownRenderResult, MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize } from '../../../../../nls.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -111,6 +111,7 @@ export interface IChatViewWelcomeContent { icon?: ThemeIcon; title: string; message: IMarkdownString | ((disposables: DisposableStore) => HTMLElement); + additionalMessage?: string | IMarkdownString; tips?: IMarkdownString; } @@ -158,21 +159,19 @@ export class ChatViewWelcomePart extends Disposable { if (typeof content.message === 'function') { dom.append(message, content.message(this._register(new DisposableStore()))); } else { - const messageResult = this._register(renderer.render(content.message)); - const firstLink = options?.firstLinkToButton ? messageResult.element.querySelector('a') : undefined; - if (firstLink) { - const target = firstLink.getAttribute('data-href'); - const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); - button.label = firstLink.textContent ?? ''; - if (target) { - this._register(button.onDidClick(() => { - this.openerService.open(target, { allowCommands: true }); - })); - } - firstLink.replaceWith(button.element); - } - + const messageResult = this.renderMarkdownMessageContent(renderer, content.message, options); dom.append(message, messageResult.element); + + } + + // Additional message + if (typeof content.additionalMessage === 'string') { + const element = $(''); + element.textContent = content.additionalMessage; + dom.append(message, element); + } else if (content.additionalMessage) { + const additionalMessageResult = this.renderMarkdownMessageContent(renderer, content.additionalMessage, options); + dom.append(message, additionalMessageResult.element); } // Tips @@ -185,4 +184,21 @@ export class ChatViewWelcomePart extends Disposable { this.logService.error('Failed to render chat view welcome content', err); } } + + private renderMarkdownMessageContent(renderer: MarkdownRenderer, content: IMarkdownString, options: IChatViewWelcomeRenderOptions | undefined): IMarkdownRenderResult { + const messageResult = this._register(renderer.render(content)); + const firstLink = options?.firstLinkToButton ? messageResult.element.querySelector('a') : undefined; + if (firstLink) { + const target = firstLink.getAttribute('data-href'); + const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); + button.label = firstLink.textContent ?? ''; + if (target) { + this._register(button.onDidClick(() => { + this.openerService.open(target, { allowCommands: true }); + })); + } + firstLink.replaceWith(button.element); + } + return messageResult; + } } diff --git a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index e0e7383bf40..f1b94e924b6 100644 --- a/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/code/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -27,9 +27,9 @@ export interface IChatViewsWelcomeContributionRegistry { register(descriptor: IChatViewsWelcomeDescriptor): void; } -class ChatViewsWelcomeContributionRegistry implements IChatViewsWelcomeContributionRegistry { +class ChatViewsWelcomeContributionRegistry extends Disposable implements IChatViewsWelcomeContributionRegistry { private readonly descriptors: IChatViewsWelcomeDescriptor[] = []; - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; public register(descriptor: IChatViewsWelcomeDescriptor): void { diff --git a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts index 6ef8775d594..4237022ac05 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -7,7 +7,7 @@ import { findLast } from '../../../../base/common/arraysFind.js'; import { timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; @@ -24,7 +24,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ChatContextKeys } from './chatContextKeys.js'; -import { IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; +import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -51,8 +51,6 @@ export interface IChatAgentData { extensionDisplayName: string; /** The agent invoked when no agent is specified */ isDefault?: boolean; - /** The default agent when "agent-mode" is enabled */ - isToolsAgent?: boolean; /** This agent is not contributed in package.json, but is registered dynamically */ isDynamic?: boolean; /** This agent is contributed from core and not from an extension */ @@ -60,6 +58,7 @@ export interface IChatAgentData { metadata: IChatAgentMetadata; slashCommands: IChatAgentCommand[]; locations: ChatAgentLocation[]; + modes: ChatMode[]; disambiguation: { category: string; description: string; examples: string[] }[]; } @@ -69,18 +68,10 @@ export interface IChatWelcomeMessageContent { message: IMarkdownString; } -export function isChatWelcomeMessageContent(obj: any): obj is IChatWelcomeMessageContent { - return obj && - ThemeIcon.isThemeIcon(obj.icon) && - typeof obj.title === 'string' && - isMarkdownString(obj.message); -} - export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; setRequestPaused?(requestId: string, isPaused: boolean): void; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideWelcomeMessage?(token: CancellationToken): ProviderResult; provideChatTitle?: (history: IChatAgentHistoryEntry[], token: CancellationToken) => Promise; provideSampleQuestions?(location: ChatAgentLocation, token: CancellationToken): ProviderResult; } @@ -127,7 +118,7 @@ export interface IChatAgentMetadata { followupPlaceholder?: string; isSticky?: boolean; requester?: IChatRequesterInformation; - welcomeMessageContent?: IChatWelcomeMessageContent; + additionalWelcomeMessage?: string | IMarkdownString; } @@ -147,6 +138,9 @@ export interface IChatAgentRequest { rejectedConfirmationData?: any[]; userSelectedModelId?: string; userSelectedTools?: string[]; + userSelectedTools2?: Record; + toolSelectionIsExclusive?: boolean; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IChatQuestion { @@ -235,9 +229,10 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _agentsContextKeys = new Set(); private readonly _hasDefaultAgent: IContextKey; + private readonly _extensionAgentRegistered: IContextKey; private readonly _defaultAgentRegistered: IContextKey; private readonly _editingAgentRegistered: IContextKey; - private readonly _hasToolsAgentContextKey: IContextKey; + private _hasToolsAgent = false; private _chatParticipantDetectionProviders = new Map(); @@ -246,6 +241,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { ) { super(); this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService); + this._extensionAgentRegistered = ChatContextKeys.extensionParticipantRegistered.bindTo(this.contextKeyService); this._defaultAgentRegistered = ChatContextKeys.panelParticipantRegistered.bindTo(this.contextKeyService); this._editingAgentRegistered = ChatContextKeys.editingParticipantRegistered.bindTo(this.contextKeyService); this._register(contextKeyService.onDidChangeContext((e) => { @@ -253,8 +249,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._updateContextKeys(); } })); - - this._hasToolsAgentContextKey = ChatContextKeys.Editing.hasToolsAgent.bindTo(contextKeyService); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -300,23 +294,29 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private _updateContextKeys(): void { let editingAgentRegistered = false; + let extensionAgentRegistered = false; let defaultAgentRegistered = false; let toolsAgentRegistered = false; for (const agent of this.getAgents()) { - if (agent.isDefault && agent.locations.includes(ChatAgentLocation.EditingSession)) { - editingAgentRegistered = true; - if (agent.isToolsAgent) { + if (agent.isDefault) { + if (!agent.isCore) { + extensionAgentRegistered = true; + } + if (agent.modes.includes(ChatMode.Agent)) { toolsAgentRegistered = true; + } else if (agent.modes.includes(ChatMode.Edit)) { + editingAgentRegistered = true; + } else { + defaultAgentRegistered = true; } - } else if (agent.isDefault) { - defaultAgentRegistered = true; } } this._editingAgentRegistered.set(editingAgentRegistered); this._defaultAgentRegistered.set(defaultAgentRegistered); - if (toolsAgentRegistered !== this._hasToolsAgentContextKey.get()) { - this._hasToolsAgentContextKey.set(toolsAgentRegistered); - this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); + this._extensionAgentRegistered.set(extensionAgentRegistered); + if (toolsAgentRegistered !== this._hasToolsAgent) { + this._hasToolsAgent = toolsAgentRegistered; + this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.Panel, ChatMode.Agent)); } } @@ -381,13 +381,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } - getDefaultAgent(location: ChatAgentLocation, mode?: ChatMode): IChatAgent | undefined { - if (mode === ChatMode.Edit || mode === ChatMode.Agent) { - location = ChatAgentLocation.EditingSession; - } - + getDefaultAgent(location: ChatAgentLocation, mode: ChatMode = ChatMode.Ask): IChatAgent | undefined { return this._preferExtensionAgent(this.getActivatedAgents().filter(a => { - if ((mode === ChatMode.Agent) !== !!a.isToolsAgent) { + if (mode && !a.modes.includes(mode)) { return false; } @@ -396,7 +392,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } public get hasToolsAgent(): boolean { - return !!this._hasToolsAgentContextKey.get(); + return !!this._hasToolsAgent; } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { @@ -492,11 +488,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data?.impl) { - throw new Error(`No activated agent with id "${id}"`); - } - - if (!data.impl?.provideFollowups) { + if (!data?.impl?.provideFollowups) { return []; } @@ -587,11 +579,11 @@ export class MergedChatAgent implements IChatAgent { get extensionPublisherDisplayName() { return this.data.publisherDisplayName; } get extensionDisplayName(): string { return this.data.extensionDisplayName; } get isDefault(): boolean | undefined { return this.data.isDefault; } - get isToolsAgent(): boolean | undefined { return this.data.isToolsAgent; } get isCore(): boolean | undefined { return this.data.isCore; } get metadata(): IChatAgentMetadata { return this.data.metadata; } get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } get locations(): ChatAgentLocation[] { return this.data.locations; } + get modes(): ChatMode[] { return this.data.modes; } get disambiguation(): { category: string; description: string; examples: string[] }[] { return this.data.disambiguation; } async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { @@ -612,14 +604,6 @@ export class MergedChatAgent implements IChatAgent { return []; } - provideWelcomeMessage(token: CancellationToken): ProviderResult { - if (this.impl.provideWelcomeMessage) { - return this.impl.provideWelcomeMessage(token); - } - - return undefined; - } - provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): ProviderResult { if (this.impl.provideSampleQuestions) { return this.impl.provideSampleQuestions(location, token); diff --git a/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index e761fc19358..749850a7aee 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -24,6 +24,8 @@ export interface ICodeMapperCodeBlock { export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; readonly chatRequestId?: string; + readonly chatRequestModel?: string; + readonly chatSessionId?: string; readonly location?: string; } diff --git a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index bf8c4ab04ba..9e7c2e5d52c 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { RemoteNameContext } from '../../../common/contextkeys.js'; -import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; export namespace ChatContextKeys { export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); @@ -30,13 +30,13 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); - export const inUnifiedChat = new RawContextKey('inUnifiedChat', false, { type: 'boolean', description: localize('inUnifiedChat', "True when focus is in the unified chat widget, false otherwise.") }); - export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); + export const hasPromptFile = new RawContextKey('chatPromptFileAttached', false, { type: 'boolean', description: localize('chatPromptFileAttachedContextDescription', "True when the chat has a prompt file attached.") }); export const chatMode = new RawContextKey('chatMode', ChatMode.Ask, { type: 'string', description: localize('chatMode', "The current chat mode.") }); - export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection + export const supported = ContextKeyExpr.or(IsWebContext.negate(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); + export const extensionParticipantRegistered = new RawContextKey('chatPanelExtensionParticipantRegistered', false, { type: 'boolean', description: localize('chatPanelExtensionParticipantRegistered', "True when a default chat participant is registered for the panel from an extension.") }); export const panelParticipantRegistered = new RawContextKey('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") }); export const editingParticipantRegistered = new RawContextKey('chatEditingParticipantRegistered', false, { type: 'boolean', description: localize('chatEditingParticipantRegistered', "True when a default chat participant is registered for editing.") }); export const chatEditingCanUndo = new RawContextKey('chatEditingCanUndo', false, { type: 'boolean', description: localize('chatEditingCanUndo', "True when it is possible to undo an interaction in the editing panel.") }); @@ -52,40 +52,24 @@ export namespace ChatContextKeys { export const Setup = { hidden: new RawContextKey('chatSetupHidden', false, true), // True when chat setup is explicitly hidden. - installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed. - fromDialog: ContextKeyExpr.has('config.chat.setupFromDialog'), + installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed and enabled. + disabled: new RawContextKey('chatSetupDisabled', false, true) // True when the chat extension is disabled. }; export const Entitlement = { signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. canSignUp: new RawContextKey('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat limited user. limited: new RawContextKey('chatPlanLimited', false, true), // True when user is a chat limited user. - pro: new RawContextKey('chatPlanPro', false, true) // True when user is a chat pro user. + pro: new RawContextKey('chatPlanPro', false, true), // True when user is a chat pro user. + proPlus: new RawContextKey('chatPlanProPlus', false, true), // True when user is a chat pro plus user. + business: new RawContextKey('chatPlanBusiness', false, true), // True when user is a chat business user. + enterprise: new RawContextKey('chatPlanEnterprise', false, true) // True when user is a chat enterprise user. }; - export const SetupViewKeys = new Set([ChatContextKeys.Setup.hidden.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Entitlement.signedOut.key, ChatContextKeys.Entitlement.canSignUp.key, ...Setup.fromDialog.keys()]); - export const SetupViewCondition = ContextKeyExpr.and( - Setup.fromDialog.negate(), - ContextKeyExpr.or( - ContextKeyExpr.and( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed.negate() - ), - ContextKeyExpr.and( - ChatContextKeys.Entitlement.canSignUp, - ChatContextKeys.Setup.installed - ), - ContextKeyExpr.and( - ChatContextKeys.Entitlement.signedOut, - ChatContextKeys.Setup.installed - ) - ))!; - export const chatQuotaExceeded = new RawContextKey('chatQuotaExceeded', false, true); export const completionsQuotaExceeded = new RawContextKey('completionsQuotaExceeded', false, true); export const Editing = { - hasToolsAgent: new RawContextKey('chatHasToolsAgent', false, { type: 'boolean', description: localize('chatEditingHasToolsAgent', "True when a tools agent is registered.") }), agentModeDisallowed: new RawContextKey('chatAgentModeDisallowed', undefined, { type: 'boolean', description: localize('chatAgentModeDisallowed', "True when agent mode is not allowed.") }), // experiment-driven disablement hasToolConfirmation: new RawContextKey('chatHasToolConfirmation', false, { type: 'boolean', description: localize('chatEditingHasToolConfirmation', "True when a tool confirmation is present.") }), }; @@ -96,16 +80,6 @@ export namespace ChatContextKeys { } export namespace ChatContextKeyExprs { - export const unifiedChatEnabled = ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`); - - export const inEditsOrUnified = ContextKeyExpr.or( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), - ChatContextKeys.inUnifiedChat); - - export const inNonUnifiedPanel = ContextKeyExpr.and( - ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - ChatContextKeys.inUnifiedChat.negate()); - export const inEditingMode = ContextKeyExpr.or( ChatContextKeys.chatMode.isEqualTo(ChatMode.Edit), ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), diff --git a/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 629bbd1cdc7..e79982344b1 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/code/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, } /** @@ -179,7 +165,7 @@ export interface IModifiedFileEntryEditorIntegration extends IDisposable { /** * Reveal the first (`true`) or last (`false`) change */ - reveal(firstOrLast: boolean): void; + reveal(firstOrLast: boolean, preserveFocus?: boolean): void; /** * Go to next change and increate `currentIndex` @@ -202,17 +188,19 @@ export interface IModifiedFileEntryEditorIntegration extends IDisposable { * Accept the change given or the nearest * @param change An opaque change object */ - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void; + acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; /** * @see `acceptNearestChange` */ - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void; + rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; /** * Toggle between diff-editor and normal editor + * @param change An opaque change object + * @param show Optional boolean to control if the diff should show */ - toggleDiff(change: IModifiedFileEntryChangeHunk | undefined): Promise; + toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise; } export interface IModifiedFileEntry { @@ -222,8 +210,9 @@ export interface IModifiedFileEntry { readonly lastModifyingRequestId: string; - readonly state: IObservable; + readonly state: IObservable; readonly isCurrentlyBeingModifiedBy: IObservable; + readonly lastModifyingResponse: IObservable; readonly rewriteRatio: IObservable; accept(transaction: ITransaction | undefined): Promise; @@ -255,7 +244,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 +270,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/code/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/code/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts index 66ea4b5a150..35ff7d7c2d7 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -8,7 +8,7 @@ import { Barrier } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -21,7 +21,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asText, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { ChatContextKeys } from './chatContextKeys.js'; @@ -31,6 +31,7 @@ import Severity from '../../../../base/common/severity.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { Mutable } from '../../../../base/common/types.js'; export const IChatEntitlementService = createDecorator('chatEntitlementService'); @@ -46,7 +47,13 @@ export enum ChatEntitlement { /** Signed-up to Limited */ Limited, /** Signed-up to Pro */ - Pro + Pro, + /** Signed-up to Pro Plus */ + ProPlus, + /** Signed-up to Business */ + Business, + /** Signed-up to Enterprise */ + Enterprise } export enum ChatSentiment { @@ -58,18 +65,6 @@ export enum ChatSentiment { Installed = 3 } -export interface IChatQuotas { - readonly chatQuotaExceeded: boolean; - readonly completionsQuotaExceeded: boolean; - readonly quotaResetDate: Date | undefined; - - readonly chatTotal?: number; - readonly completionsTotal?: number; - - readonly chatRemaining?: number; - readonly completionsRemaining?: number; -} - export interface IChatEntitlementService { _serviceBrand: undefined; @@ -81,7 +76,7 @@ export interface IChatEntitlementService { readonly onDidChangeQuotaExceeded: Event; readonly onDidChangeQuotaRemaining: Event; - readonly quotas: IChatQuotas; + readonly quotas: IQuotas; update(token: CancellationToken): Promise; @@ -90,6 +85,20 @@ export interface IChatEntitlementService { readonly sentiment: ChatSentiment; } +//#region Helper Functions + +/** + * Checks the chat entitlements to see if the user falls into the paid category + * @param chatEntitlement The chat entitlement to check + * @returns Whether or not they are a paid user + */ +export function isProUser(chatEntitlement: ChatEntitlement): boolean { + return chatEntitlement === ChatEntitlement.Pro || + chatEntitlement === ChatEntitlement.ProPlus || + chatEntitlement === ChatEntitlement.Business || + chatEntitlement === ChatEntitlement.Enterprise; +} + //#region Service Implementation const defaultChat = { @@ -108,7 +117,7 @@ const defaultChat = { interface IChatQuotasAccessor { clearQuotas(): void; - acceptQuotas(quotas: IChatQuotas): void; + acceptQuotas(quotas: IQuotas): void; } export class ChatEntitlementService extends Disposable implements IChatEntitlementService { @@ -133,6 +142,9 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme Event.filter( this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([ ChatContextKeys.Entitlement.pro.key, + ChatContextKeys.Entitlement.business.key, + ChatContextKeys.Entitlement.enterprise.key, + ChatContextKeys.Entitlement.proPlus.key, ChatContextKeys.Entitlement.limited.key, ChatContextKeys.Entitlement.canSignUp.key, ChatContextKeys.Entitlement.signedOut.key @@ -173,6 +185,12 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme get entitlement(): ChatEntitlement { if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.pro.key) === true) { return ChatEntitlement.Pro; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.business.key) === true) { + return ChatEntitlement.Business; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.enterprise.key) === true) { + return ChatEntitlement.Enterprise; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.proPlus.key) === true) { + return ChatEntitlement.ProPlus; } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.limited.key) === true) { return ChatEntitlement.Limited; } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.canSignUp.key) === true) { @@ -194,7 +212,7 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme private readonly _onDidChangeQuotaRemaining = this._register(new Emitter()); readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event; - private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }; + private _quotas: IQuotas = {}; get quotas() { return this._quotas; } private readonly chatQuotaExceededContextKey: IContextKey; @@ -206,77 +224,61 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme }; private registerListeners(): void { - const chatQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded]); - const completionsQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); + const quotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded, this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); + const cts = this._register(new MutableDisposable()); this._register(this.contextKeyService.onDidChangeContext(e => { - let changed = false; - if (e.affectsSome(chatQuotaExceededSet)) { - const newChatQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.chatQuotaExceeded); - if (typeof newChatQuotaExceeded === 'boolean' && newChatQuotaExceeded !== this._quotas.chatQuotaExceeded) { - this._quotas = { - ...this._quotas, - chatQuotaExceeded: newChatQuotaExceeded, - }; - changed = true; + if (e.affectsSome(quotaExceededSet)) { + if (cts.value) { + cts.value.cancel(); } - } - - if (e.affectsSome(completionsQuotaExceededSet)) { - const newCompletionsQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.completionsQuotaExceeded); - if (typeof newCompletionsQuotaExceeded === 'boolean' && newCompletionsQuotaExceeded !== this._quotas.completionsQuotaExceeded) { - this._quotas = { - ...this._quotas, - completionsQuotaExceeded: newCompletionsQuotaExceeded, - }; - changed = true; - } - } - - if (changed) { - this.updateContextKeys(); - this._onDidChangeQuotaExceeded.fire(); + cts.value = new CancellationTokenSource(); + this.update(cts.value.token); } })); } - acceptQuotas(quotas: IChatQuotas): void { + acceptQuotas(quotas: IQuotas): void { const oldQuota = this._quotas; this._quotas = quotas; this.updateContextKeys(); - if ( - oldQuota.chatQuotaExceeded !== this._quotas.chatQuotaExceeded || - oldQuota.completionsQuotaExceeded !== this._quotas.completionsQuotaExceeded - ) { + const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat); + const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions); + const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat); + + if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) { this._onDidChangeQuotaExceeded.fire(); } - if ( - oldQuota.chatRemaining !== this._quotas.chatRemaining || - oldQuota.completionsRemaining !== this._quotas.completionsRemaining - ) { + if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining) { this._onDidChangeQuotaRemaining.fire(); } } + private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } { + return { + changed: { + exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0), + remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining + } + }; + } + clearQuotas(): void { - if (this.quotas.chatQuotaExceeded || this.quotas.completionsQuotaExceeded) { - this.acceptQuotas({ chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }); - } + this.acceptQuotas({}); } private updateContextKeys(): void { - this.chatQuotaExceededContextKey.set(this._quotas.chatQuotaExceeded); - this.completionsQuotaExceededContextKey.set(this._quotas.completionsQuotaExceeded); + this.chatQuotaExceededContextKey.set(this._quotas.chat?.percentRemaining === 0); + this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0); } //#endregion //#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) { @@ -302,8 +304,9 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme type EntitlementClassification = { tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; - quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; - quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat requests available to the user' }; + quotaPremiumChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of premium chat requests available to the user' }; + quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of code completions available to the user' }; quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; owner: 'bpasero'; comment: 'Reporting chat entitlements'; @@ -313,16 +316,21 @@ type EntitlementEvent = { entitlement: ChatEntitlement; tid: string; quotaChat: number | undefined; + quotaPremiumChat: number | undefined; quotaCompletions: number | undefined; quotaResetDate: string | undefined; }; -interface IEntitlementsResponse { - readonly access_type_sku: string; - readonly assigned_date: string; - readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; - readonly analytics_tracking_id: string; +interface IQuotaSnapshotResponse { + readonly entitlement: number; + readonly overage_count: number; + readonly overage_permitted: boolean; + readonly percent_remaining: number; + readonly remaining: number; + readonly unlimited: boolean; +} + +interface ILegacyQuotaSnapshotResponse { readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -331,7 +339,22 @@ interface IEntitlementsResponse { readonly chat: number; readonly completions: number; }; - readonly limited_user_reset_date: string; +} + +interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly chat_enabled: boolean; + readonly copilot_plan: string; + readonly analytics_tracking_id: string; + readonly limited_user_reset_date?: string; // for Copilot Free + readonly quota_reset_date?: string; // for all other Copilot SKUs + readonly quota_snapshots?: { + chat?: IQuotaSnapshotResponse; + completions?: IQuotaSnapshotResponse; + premium_interactions?: IQuotaSnapshotResponse; + }; } interface IEntitlements { @@ -339,14 +362,21 @@ interface IEntitlements { readonly quotas?: IQuotas; } -interface IQuotas { - readonly chatTotal?: number; - readonly completionsTotal?: number; +export interface IQuotaSnapshot { + readonly total: number; + readonly percentRemaining: number; - readonly chatRemaining?: number; - readonly completionsRemaining?: number; + readonly overageEnabled: boolean; + readonly overageCount: number; + readonly unlimited: boolean; +} + +interface IQuotas { readonly resetDate?: string; + readonly chat?: IQuotaSnapshot; + readonly completions?: IQuotaSnapshot; + readonly premiumChat?: IQuotaSnapshot; } export class ChatEntitlementRequests extends Disposable { @@ -466,8 +496,17 @@ export class ChatEntitlementRequests extends Disposable { } private async doGetSessions(providerId: string): Promise { + const preferredAccountName = this.authenticationExtensionsService.getAccountPreference(defaultChat.chatExtensionId, providerId) ?? this.authenticationExtensionsService.getAccountPreference(defaultChat.extensionId, providerId); + let preferredAccount: AuthenticationSessionAccount | undefined; + for (const account of await this.authenticationService.getAccounts(providerId)) { + if (account.label === preferredAccountName) { + preferredAccount = account; + break; + } + } + try { - return await this.authenticationService.getSessions(providerId); + return await this.authenticationService.getSessions(providerId, undefined, preferredAccount); } catch (error) { // ignore - errors can throw if a provider is not registered } @@ -512,9 +551,8 @@ export class ChatEntitlementRequests extends Disposable { if (response.res.statusCode && response.res.statusCode !== 200) { this.logService.trace(`[chat entitlement]: unexpected status code ${response.res.statusCode}`); return ( - response.res.statusCode === 401 || - response.res.statusCode === 403 || - response.res.statusCode === 404 + response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked) + response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist ) ? { entitlement: ChatEntitlement.Unknown /* treat as signed out */ } : { entitlement: ChatEntitlement.Unresolved }; } @@ -547,38 +585,97 @@ export class ChatEntitlementRequests extends Disposable { entitlement = ChatEntitlement.Limited; } else if (entitlementsResponse.can_signup_for_limited) { entitlement = ChatEntitlement.Available; + } else if (entitlementsResponse.copilot_plan === 'individual') { + entitlement = ChatEntitlement.Pro; + } else if (entitlementsResponse.copilot_plan === 'individual_pro') { + entitlement = ChatEntitlement.ProPlus; + } else if (entitlementsResponse.copilot_plan === 'business') { + entitlement = ChatEntitlement.Business; + } else if (entitlementsResponse.copilot_plan === 'enterprise') { + entitlement = ChatEntitlement.Enterprise; } else if (entitlementsResponse.chat_enabled) { + // This should never happen as we exhaustively list the plans above. But if a new plan is added in the future older clients won't break entitlement = ChatEntitlement.Pro; } else { entitlement = ChatEntitlement.Unavailable; } - const chatRemaining = entitlementsResponse.limited_user_quotas?.chat; - const completionsRemaining = entitlementsResponse.limited_user_quotas?.completions; - const entitlements: IEntitlements = { entitlement, - quotas: { - chatTotal: entitlementsResponse.monthly_quotas?.chat, - completionsTotal: entitlementsResponse.monthly_quotas?.completions, - chatRemaining: typeof chatRemaining === 'number' ? Math.max(0, chatRemaining) : undefined, - completionsRemaining: typeof completionsRemaining === 'number' ? Math.max(0, completionsRemaining) : undefined, - resetDate: entitlementsResponse.limited_user_reset_date - } + quotas: this.toQuotas(entitlementsResponse) }; this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, tid: entitlementsResponse.analytics_tracking_id, - quotaChat: entitlementsResponse.limited_user_quotas?.chat, - quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, - quotaResetDate: entitlementsResponse.limited_user_reset_date + quotaChat: entitlementsResponse?.quota_snapshots?.chat?.remaining, + quotaPremiumChat: entitlementsResponse?.quota_snapshots?.premium_interactions?.remaining, + quotaCompletions: entitlementsResponse?.quota_snapshots?.completions?.remaining, + quotaResetDate: entitlementsResponse.quota_reset_date ?? entitlementsResponse.limited_user_reset_date }); return entitlements; } + private toQuotas(response: IEntitlementsResponse): IQuotas { + const quotas: Mutable = { + resetDate: response.quota_reset_date ?? response.limited_user_reset_date + }; + + // Legacy Free SKU Quota + if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') { + quotas.chat = { + total: response.monthly_quotas.chat, + percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100)), + overageEnabled: false, + overageCount: 0, + unlimited: false + }; + } + + if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') { + quotas.completions = { + total: response.monthly_quotas.completions, + percentRemaining: Math.min(100, Math.max(0, (response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100)), + overageEnabled: false, + overageCount: 0, + unlimited: false + }; + } + + // New Quota Snapshot + if (response.quota_snapshots) { + for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) { + const rawQuotaSnapshot = response.quota_snapshots[quotaType]; + if (!rawQuotaSnapshot) { + continue; + } + const quotaSnapshot: IQuotaSnapshot = { + total: rawQuotaSnapshot.entitlement, + percentRemaining: Math.min(100, Math.max(0, rawQuotaSnapshot.percent_remaining)), + overageEnabled: rawQuotaSnapshot.overage_permitted, + overageCount: rawQuotaSnapshot.overage_count, + unlimited: rawQuotaSnapshot.unlimited + }; + + switch (quotaType) { + case 'chat': + quotas.chat = quotaSnapshot; + break; + case 'completions': + quotas.completions = quotaSnapshot; + break; + case 'premium_interactions': + quotas.premiumChat = quotaSnapshot; + break; + } + } + } + + return quotas; + } + private async request(url: string, type: 'GET', body: undefined, session: AuthenticationSession, token: CancellationToken): Promise; private async request(url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; private async request(url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { @@ -607,15 +704,7 @@ export class ChatEntitlementRequests extends Disposable { this.context.update({ entitlement: this.state.entitlement }); if (state.quotas) { - this.chatQuotasAccessor.acceptQuotas({ - chatQuotaExceeded: typeof state.quotas.chatRemaining === 'number' ? state.quotas.chatRemaining <= 0 : false, - completionsQuotaExceeded: typeof state.quotas.completionsRemaining === 'number' ? state.quotas.completionsRemaining <= 0 : false, - quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : undefined, - chatTotal: state.quotas.chatTotal, - completionsTotal: state.quotas.completionsTotal, - chatRemaining: state.quotas.chatRemaining, - completionsRemaining: state.quotas.completionsRemaining - }); + this.chatQuotasAccessor.acceptQuotas(state.quotas); } } @@ -756,6 +845,7 @@ export interface IChatEntitlementContextState { entitlement: ChatEntitlement; hidden?: boolean; installed?: boolean; + disabled?: boolean; registered?: boolean; } @@ -767,8 +857,12 @@ export class ChatEntitlementContext extends Disposable { private readonly signedOutContextKey: IContextKey; private readonly limitedContextKey: IContextKey; private readonly proContextKey: IContextKey; + private readonly proPlusContextKey: IContextKey; + private readonly businessContextKey: IContextKey; + private readonly enterpriseContextKey: IContextKey; private readonly hiddenContext: IContextKey; private readonly installedContext: IContextKey; + private readonly disabledContext: IContextKey; private _state: IChatEntitlementContextState; private suspendedState: IChatEntitlementContextState | undefined = undefined; @@ -794,8 +888,12 @@ export class ChatEntitlementContext extends Disposable { this.signedOutContextKey = ChatContextKeys.Entitlement.signedOut.bindTo(contextKeyService); this.limitedContextKey = ChatContextKeys.Entitlement.limited.bindTo(contextKeyService); this.proContextKey = ChatContextKeys.Entitlement.pro.bindTo(contextKeyService); + this.proPlusContextKey = ChatContextKeys.Entitlement.proPlus.bindTo(contextKeyService); + this.businessContextKey = ChatContextKeys.Entitlement.business.bindTo(contextKeyService); + this.enterpriseContextKey = ChatContextKeys.Entitlement.enterprise.bindTo(contextKeyService); this.hiddenContext = ChatContextKeys.Setup.hidden.bindTo(contextKeyService); this.installedContext = ChatContextKeys.Setup.installed.bindTo(contextKeyService); + this.disabledContext = ChatContextKeys.Setup.disabled.bindTo(contextKeyService); this._state = this.storageService.getObject(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; @@ -815,18 +913,23 @@ export class ChatEntitlementContext extends Disposable { } const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); + this.update({ + // TODO@bpasero considering enablement state here as well for historic reasons, should revisit when Copilot can be enabled/disabled more generally + installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local), + disabled: !!defaultChatExtension?.local && !this.extensionEnablementService.isEnabled(defaultChatExtension.local) + }); })); } - update(context: { installed: boolean }): Promise; + update(context: { installed: boolean; disabled: boolean }): Promise; update(context: { hidden: boolean }): Promise; update(context: { entitlement: ChatEntitlement }): Promise; - update(context: { installed?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { + update(context: { installed?: boolean; disabled?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`); - if (typeof context.installed === 'boolean') { + if (typeof context.installed === 'boolean' && typeof context.disabled === 'boolean') { this._state.installed = context.installed; + this._state.disabled = context.disabled; if (context.installed) { context.hidden = false; // allows to fallback if the extension is uninstalled @@ -840,7 +943,7 @@ export class ChatEntitlementContext extends Disposable { if (typeof context.entitlement === 'number') { this._state.entitlement = context.entitlement; - if (this._state.entitlement === ChatEntitlement.Limited || this._state.entitlement === ChatEntitlement.Pro) { + if (this._state.entitlement === ChatEntitlement.Limited || isProUser(this._state.entitlement)) { this._state.registered = true; } else if (this._state.entitlement === ChatEntitlement.Available) { this._state.registered = false; // only reset when signed-in user can sign-up for limited @@ -865,8 +968,12 @@ export class ChatEntitlementContext extends Disposable { this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); + this.proPlusContextKey.set(this._state.entitlement === ChatEntitlement.ProPlus); + this.businessContextKey.set(this._state.entitlement === ChatEntitlement.Business); + this.enterpriseContextKey.set(this._state.entitlement === ChatEntitlement.Enterprise); this.hiddenContext.set(!!this._state.hidden); this.installedContext.set(!!this._state.installed); + this.disabledContext.set(!!this._state.disabled); this._onDidChange.fire(); } @@ -884,3 +991,4 @@ export class ChatEntitlementContext extends Disposable { } //#endregion + diff --git a/code/src/vs/workbench/contrib/chat/common/chatModel.ts b/code/src/vs/workbench/contrib/chat/common/chatModel.ts index c393e9a849b..42dd90f95db 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; @@ -27,39 +28,60 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; -import { ChatAgentLocation } from './constants.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; -export interface IBaseChatRequestVariableEntry { +interface IBaseChatRequestVariableEntry { id: string; fullName?: string; icon?: ThemeIcon; name: string; modelDescription?: string; + + /** + * The offset-range in the prompt. This means this entry has been explicitly typed out + * by the user. + */ range?: IOffsetRange; value: IChatRequestVariableValue; references?: IChatContentReference[]; - mimeType?: string; - // TODO these represent different kinds, should be extracted to new interfaces with kind tags - kind?: never; - isFile?: boolean; - isDirectory?: boolean; - isTool?: boolean; - isImage?: boolean; - isOmitted?: boolean; + omittedState?: OmittedState; +} + +export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry { + kind: 'generic'; +} + +export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry { + kind: 'directory'; +} + +export interface IChatRequestFileEntry extends IBaseChatRequestVariableEntry { + kind: 'file'; } -export interface IChatRequestImplicitVariableEntry extends Omit { +export const enum OmittedState { + NotOmitted, + Partial, + Full, +} + +export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'tool'; +} + +export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'implicit'; readonly isFile: true; readonly value: URI | Location | undefined; readonly isSelection: boolean; + readonly isPromptFile: boolean; enabled: boolean; } -export interface IChatRequestPasteVariableEntry extends Omit { +export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'paste'; code: string; language: string; @@ -75,20 +97,27 @@ export interface IChatRequestPasteVariableEntry extends Omit { +export interface ISymbolVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'symbol'; readonly value: Location; readonly symbolKind: SymbolKind; } -export interface ICommandResultVariableEntry extends Omit { +export interface ICommandResultVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'command'; } -export interface IImageVariableEntry extends Omit { +export interface IImageVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'image'; readonly isPasted?: boolean; readonly isURL?: boolean; + readonly mimeType?: string; +} + +export interface INotebookOutputVariableEntry extends Omit { + readonly kind: 'notebookOutput'; + readonly outputIndex?: number; + readonly mimeType?: string; } export interface IDiagnosticVariableEntryFilterData { @@ -99,6 +128,16 @@ export interface IDiagnosticVariableEntryFilterData { readonly filterRange?: IRange; } +/** + * Chat variable that represents an attached prompt file. + */ +export interface IPromptVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'file'; + readonly value: URI | Location; + readonly isRoot: boolean; + readonly modelDescription: string; +} + export namespace IDiagnosticVariableEntryFilterData { export const icon = Codicon.error; @@ -117,8 +156,7 @@ export namespace IDiagnosticVariableEntryFilterData { name: label(data), icon, value: data, - kind: 'diagnostic' as const, - range: data.filterRange ? new OffsetRange(data.filterRange.startLineNumber, data.filterRange.endLineNumber) : undefined, + kind: 'diagnostic', ...data, }; } @@ -154,11 +192,17 @@ export namespace IDiagnosticVariableEntryFilterData { } } -export interface IDiagnosticVariableEntry extends Omit, IDiagnosticVariableEntryFilterData { +export interface IDiagnosticVariableEntry extends IBaseChatRequestVariableEntry, IDiagnosticVariableEntryFilterData { readonly kind: 'diagnostic'; } -export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IBaseChatRequestVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry; +export interface IElementVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'element'; +} + +export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry + | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry + | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -172,10 +216,22 @@ export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IIm return obj.kind === 'image'; } +export function isNotebookOutputVariableEntry(obj: IChatRequestVariableEntry): obj is INotebookOutputVariableEntry { + return obj.kind === 'notebookOutput'; +} + +export function isElementVariableEntry(obj: IChatRequestVariableEntry): obj is IElementVariableEntry { + return obj.kind === 'element'; +} + export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry { return obj.kind === 'diagnostic'; } +export function isChatRequestFileEntry(obj: IChatRequestVariableEntry): obj is IChatRequestFileEntry { + return obj.kind === 'file'; +} + export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry { const entry = obj as IChatRequestVariableEntry; return typeof entry === 'object' && @@ -202,6 +258,7 @@ export interface IChatRequestModel { readonly attachedContext?: IChatRequestVariableEntry[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; + readonly editedFileEvents?: IChatAgentEditedFileEvent[]; shouldBeRemovedOnSend: IChatRequestDisablement | undefined; } @@ -252,7 +309,8 @@ export type IChatProgressHistoryResponseContent = | IChatTask | IChatTextEditGroup | IChatNotebookEditGroup - | IChatConfirmation; + | IChatConfirmation + | IChatExtensionsContent; /** * "Normal" progress kinds that are rendered as parts of the stream of content. @@ -330,18 +388,42 @@ export type ChatResponseModelChangeReason = const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; -export class ChatRequestModel implements IChatRequestModel { - - public response: ChatResponseModel | undefined; +export interface IChatRequestModelParameters { + session: ChatModel; + message: IParsedChatRequest; + variableData: IChatRequestVariableData; + timestamp: number; + attempt?: number; + confirmation?: string; + locationData?: IChatLocationData; + attachedContext?: IChatRequestVariableEntry[]; + isCompleteAddedRequest?: boolean; + modelId?: string; + restoredId?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; +} +export class ChatRequestModel implements IChatRequestModel { public readonly id: string; - - public get session() { + public response: ChatResponseModel | undefined; + public shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + public readonly timestamp: number; + public readonly message: IParsedChatRequest; + public readonly isCompleteAddedRequest: boolean; + public readonly modelId?: string; + + private _session: ChatModel; + private readonly _attempt: number; + private _variableData: IChatRequestVariableData; + private readonly _confirmation?: string; + private readonly _locationData?: IChatLocationData; + private readonly _attachedContext?: IChatRequestVariableEntry[]; + private readonly _editedFileEvents?: IChatAgentEditedFileEvent[]; + + public get session(): ChatModel { return this._session; } - public shouldBeRemovedOnSend: IChatRequestDisablement | undefined; - public get username(): string { return this.session.requesterUsername; } @@ -374,21 +456,23 @@ export class ChatRequestModel implements IChatRequestModel { return this._attachedContext; } - constructor( - private _session: ChatModel, - public readonly message: IParsedChatRequest, - private _variableData: IChatRequestVariableData, - public readonly timestamp: number, - private _attempt: number = 0, - private _confirmation?: string, - private _locationData?: IChatLocationData, - private _attachedContext?: IChatRequestVariableEntry[], - public readonly isCompleteAddedRequest = false, - public readonly modelId?: string, - restoredId?: string, - ) { - this.id = restoredId ?? 'request_' + generateUuid(); - // this.timestamp = Date.now(); + public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined { + return this._editedFileEvents; + } + + constructor(params: IChatRequestModelParameters) { + this._session = params.session; + this.message = params.message; + this._variableData = params.variableData; + this.timestamp = params.timestamp; + this._attempt = params.attempt ?? 0; + this._confirmation = params.confirmation; + this._locationData = params.locationData; + this._attachedContext = params.attachedContext; + this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; + this.modelId = params.modelId; + this.id = params.restoredId ?? 'request_' + generateUuid(); + this._editedFileEvents = params.editedFileEvents; } adoptTo(session: ChatModel) { @@ -457,6 +541,7 @@ class AbstractResponse implements IResponse { case 'codeblockUri': case 'toolInvocation': case 'toolInvocationSerialized': + case 'extensions': case 'undoStop': // Ignore continue; @@ -652,12 +737,39 @@ export class Response extends AbstractResponse implements IDisposable { } } +export interface IChatResponseModelParameters { + responseContent: IMarkdownString | ReadonlyArray; + session: ChatModel; + agent?: IChatAgentData; + slashCommand?: IChatAgentCommand; + requestId: string; + isComplete?: boolean; + isCanceled?: boolean; + vote?: ChatAgentVoteDirection; + voteDownReason?: ChatAgentVoteDownReason; + result?: IChatAgentResult; + followups?: ReadonlyArray; + isCompleteAddedRequest?: boolean; + shouldBeRemovedOnSend?: IChatRequestDisablement; + restoredId?: string; +} export class ChatResponseModel extends Disposable implements IChatResponseModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; public readonly id: string; + public readonly requestId: string; + private _session: ChatModel; + private _agent: IChatAgentData | undefined; + private _slashCommand: IChatAgentCommand | undefined; + private _isComplete: boolean; + private _isCanceled: boolean; + private _vote?: ChatAgentVoteDirection; + private _voteDownReason?: ChatAgentVoteDownReason; + private _result?: IChatAgentResult; + private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + public readonly isCompleteAddedRequest: boolean; public get session() { return this._session; @@ -778,31 +890,28 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel /** Functions run once the chat response is unpaused. */ private bufferedPauseContent?: (() => void)[]; - constructor( - _response: IMarkdownString | ReadonlyArray, - private _session: ChatModel, - private _agent: IChatAgentData | undefined, - private _slashCommand: IChatAgentCommand | undefined, - public readonly requestId: string, - private _isComplete: boolean = false, - private _isCanceled = false, - private _vote?: ChatAgentVoteDirection, - private _voteDownReason?: ChatAgentVoteDownReason, - private _result?: IChatAgentResult, - followups?: ReadonlyArray, - public readonly isCompleteAddedRequest = false, - private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined = undefined, - restoredId?: string - ) { + constructor(params: IChatResponseModelParameters) { super(); + this._session = params.session; + this._agent = params.agent; + this._slashCommand = params.slashCommand; + this.requestId = params.requestId; + this._isComplete = params.isComplete ?? false; + this._isCanceled = params.isCanceled ?? false; + this._vote = params.vote; + this._voteDownReason = params.voteDownReason; + this._result = params.result; + this._followups = params.followups ? [...params.followups] : undefined; + this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; + this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend; + // If we are creating a response with some existing content, consider it stale - this._isStale = Array.isArray(_response) && (_response.length !== 0 || isMarkdownString(_response) && _response.value.length !== 0); + this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0); - this._followups = followups ? [...followups] : undefined; - this._response = this._register(new Response(_response)); + this._response = this._register(new Response(params.responseContent)); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason))); - this.id = restoredId ?? 'response_' + generateUuid(); + this.id = params.restoredId ?? 'response_' + generateUuid(); } /** @@ -990,6 +1099,8 @@ export interface ISerializableChatRequestData { contentReferences?: ReadonlyArray; codeCitations?: ReadonlyArray; timestamp?: number; + confirmation?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IExportableChatData { @@ -1078,6 +1189,10 @@ function normalizeOldFields(raw: ISerializableChatDataIn): void { raw.lastMessageDate = getLastYearDate(); } } + + if ((raw.initialLocation as any) === 'editing-session') { + raw.initialLocation = ChatAgentLocation.Panel; + } } function getLastYearDate(): number { @@ -1255,7 +1370,7 @@ export class ChatModel extends Disposable implements IChatModel { } private get _defaultAgent() { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); + return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, ChatMode.Ask); } get requesterUsername(): string { @@ -1341,7 +1456,23 @@ export class ChatModel extends Disposable implements IChatModel { this.chatEditingService.startOrContinueGlobalEditingSession(this) : this.chatEditingService.createEditingSession(this); this._editingSession = new ObservablePromise(editingSessionPromise); - this._editingSession.promise.then(editingSession => this._store.isDisposed ? editingSession.dispose() : this._register(editingSession)); + this._editingSession.promise.then(editingSession => { + this._store.isDisposed ? editingSession.dispose() : this._register(editingSession); + }); + } + + private currentEditedFileEvents = new ResourceMap(); + notifyEditingAction(action: IChatEditingSessionAction): void { + const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep : + action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo : + action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null; + if (state === null) { + return; + } + + if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) { + this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri }); + } } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -1360,7 +1491,15 @@ export class ChatModel extends Disposable implements IChatModel { // Old messages don't have variableData, or have it in the wrong (non-array) shape const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, undefined, raw.requestId); + const request = new ChatRequestModel({ + session: this, + message: parsedRequest, + variableData, + timestamp: raw.timestamp ?? -1, + restoredId: raw.requestId, + confirmation: raw.confirmation, + editedFileEvents: raw.editedFileEvents, + }); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format @@ -1370,7 +1509,20 @@ export class ChatModel extends Disposable implements IChatModel { const result = 'responseErrorDetails' in raw ? // eslint-disable-next-line local/code-no-dangerous-type-assertions { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; - request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, raw.voteDownReason, result, raw.followups, undefined, undefined, raw.responseId); + request.response = new ChatResponseModel({ + responseContent: raw.response ?? [new MarkdownString(raw.response)], + session: this, + agent, + slashCommand: raw.slashCommand, + requestId: request.id, + isComplete: true, + isCanceled: raw.isCanceled, + vote: raw.vote, + voteDownReason: raw.voteDownReason, + result, + followups: raw.followups, + restoredId: raw.responseId + }); request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? request.response.applyReference(revive(raw.usedContext)); @@ -1396,6 +1548,7 @@ export class ChatModel extends Disposable implements IChatModel { // Old variables format if (v && 'values' in v && Array.isArray(v.values)) { return { + kind: 'generic', id: v.id ?? '', name: v.name, value: v.values[0]?.value, @@ -1493,8 +1646,29 @@ export class ChatModel extends Disposable implements IChatModel { } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string): ChatRequestModel { - const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, isCompleteAddedRequest, modelId); - request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest); + const editedFileEvents = [...this.currentEditedFileEvents.values()]; + this.currentEditedFileEvents.clear(); + const request = new ChatRequestModel({ + session: this, + message, + variableData, + timestamp: Date.now(), + attempt, + confirmation, + locationData, + attachedContext: attachments, + isCompleteAddedRequest, + modelId, + editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined, + }); + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + agent: chatAgent, + slashCommand, + requestId: request.id, + isCompleteAddedRequest + }); this._requests.push(request); this._lastMessageDate = Date.now(); @@ -1514,7 +1688,7 @@ export class ChatModel extends Disposable implements IChatModel { adoptRequest(request: ChatRequestModel): void { // this doesn't use `removeRequest` because it must not dispose the request object const oldOwner = request.session; - const index = oldOwner._requests.findIndex(candidate => candidate.id === request.id); + const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id); if (index === -1) { return; @@ -1532,7 +1706,11 @@ export class ChatModel extends Disposable implements IChatModel { acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { if (!request.response) { - request.response = new ChatResponseModel([], this, undefined, undefined, request.id); + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + requestId: request.id + }); } if (request.response.isComplete) { @@ -1551,6 +1729,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'warning' || progress.kind === 'progressTask' || progress.kind === 'confirmation' || + progress.kind === 'extensions' || progress.kind === 'toolInvocation' ) { request.response.updateContent(progress, quiet); @@ -1586,7 +1765,11 @@ export class ChatModel extends Disposable implements IChatModel { setResponse(request: ChatRequestModel, result: IChatAgentResult): void { if (!request.response) { - request.response = new ChatResponseModel([], this, undefined, undefined, request.id); + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + requestId: request.id + }); } request.response.setResult(result); @@ -1625,7 +1808,7 @@ export class ChatModel extends Disposable implements IChatModel { requests: this._requests.map((r): ISerializableChatRequestData => { const message = { ...r.message, - parts: r.message.parts.map(p => p && 'toJSON' in p ? (p.toJSON as Function)() : p) + parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p) }; const agent = r.response?.agent; const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() : @@ -1658,7 +1841,9 @@ export class ChatModel extends Disposable implements IChatModel { usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences, codeCitations: r.response?.codeCitations, - timestamp: r.timestamp + timestamp: r.timestamp, + confirmation: r.confirmation, + editedFileEvents: r.editedFileEvents, }; }), }; @@ -1737,3 +1922,14 @@ export function getCodeCitationsMessage(citations: ReadonlyArray(this, { progress: 0 }); + constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; this.invocationMessage = invocationMessage; this.pastTenseMessage = preparedInvocation?.pastTenseMessage; + this.originMessage = preparedInvocation?.originMessage; this._confirmationMessages = preparedInvocation?.confirmationMessages; this.presentation = preparedInvocation?.presentation; this.toolSpecificData = preparedInvocation?.toolSpecificData; @@ -84,17 +89,27 @@ export class ChatToolInvocation implements IChatToolInvocation { return this._confirmationMessages; } + public acceptProgress(step: IToolProgressStep) { + const prev = this.progress.get(); + this.progress.set({ + progress: step.increment ? (prev.progress + step.increment) : prev.progress, + message: step.message, + }, undefined); + } + public toJSON(): IChatToolInvocationSerialized { return { kind: 'toolInvocationSerialized', presentation: this.presentation, invocationMessage: this.invocationMessage, pastTenseMessage: this.pastTenseMessage, + originMessage: this.originMessage, isConfirmed: this._isConfirmed, isComplete: this._isComplete, resultDetails: this._resultDetails, toolSpecificData: this.toolSpecificData, toolCallId: this.toolCallId, + toolId: this.toolId, }; } } diff --git a/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index ecf72050af0..e88611e9867 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -7,15 +7,16 @@ import { OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IChatAgentData, IChatAgentService } from './chatAgents.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatVariablesService, IDynamicVariable } from './chatVariables.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; -import { ILanguageModelToolsService } from './languageModelToolsService.js'; +import { IToolData } from './languageModelToolsService.js'; +import { IPromptsService } from './promptSyntax/service/types.js'; const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) -const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command +const slashReg = /^\/([\w_\-\.:]+)(?=(\s|$|\b))/i; // A / command export interface IChatParserContext { /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ @@ -28,12 +29,15 @@ export class ChatRequestParser { @IChatAgentService private readonly agentService: IChatAgentService, @IChatVariablesService private readonly variableService: IChatVariablesService, @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService, - @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, + @IPromptsService private readonly promptsService: IPromptsService, ) { } parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest { const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls + const toolsByName = new Map((this.variableService.getSelectedTools(sessionId)) + .filter(t => t.canBeReferencedInPrompt && t.toolReferenceName) + .map(t => [t.toolReferenceName!, t])); let lineNumber = 1; let column = 1; @@ -43,7 +47,7 @@ export class ChatRequestParser { let newPart: IParsedChatRequestPart | undefined; if (previousChar.match(/\s/) || i === 0) { if (char === chatVariableLeader) { - newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); + newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName); } else if (char === chatAgentLeader) { newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { @@ -141,7 +145,7 @@ export class ChatRequestParser { return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); } - private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestToolPart | undefined { + private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray, toolsByName: ReadonlyMap): ChatRequestAgentPart | ChatRequestToolPart | undefined { const nextVariableMatch = message.match(variableReg); if (!nextVariableMatch) { return; @@ -151,22 +155,30 @@ export class ChatRequestParser { const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const tool = this.toolsService.getToolByName(name); - if (tool && tool.canBeReferencedInPrompt) { + const tool = toolsByName.get(name); + if (tool) { return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon); } return; } - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | ChatRequestSlashPromptPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; } - if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) { - // Only one slash command allowed + if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) { + // no other part than agent or non-whitespace text allowed: that also means no other slash command + return; + } + + // only whitespace after the last part + const previousPart = parts.at(-1); + const previousPartEnd = previousPart?.range.endExclusive ?? 0; + const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); + if (textSincePreviousPart.trim() !== '') { return; } @@ -176,18 +188,6 @@ export class ChatRequestParser { const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); if (usedAgent) { - // The slash command must come immediately after the agent - if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart))) { - return; - } - - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); - if (textSincePreviousPart.trim() !== '') { - return; - } - const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand @@ -208,8 +208,13 @@ export class ChatRequestParser { return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); } } - } + // if there's no agent, check if it's a prompt command + const promptCommand = this.promptsService.asPromptSlashCommand(command); + if (promptCommand) { + return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, promptCommand); + } + } return; } diff --git a/code/src/vs/workbench/contrib/chat/common/chatService.ts b/code/src/vs/workbench/contrib/chat/common/chatService.ts index 4ba63bfd57e..e2a930da330 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatService.ts @@ -7,6 +7,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; @@ -231,9 +232,11 @@ export interface IChatToolInvocation { confirmed: DeferredPromise; /** A 3-way: undefined=don't know yet. */ isConfirmed: boolean | undefined; + originMessage: string | IMarkdownString | undefined; invocationMessage: string | IMarkdownString; pastTenseMessage: string | IMarkdownString | undefined; resultDetails: IToolResult['toolResultDetails']; + progress: IObservable<{ message?: string | IMarkdownString; progress: number }>; readonly toolId: string; readonly toolCallId: string; @@ -250,14 +253,21 @@ export interface IChatToolInvocationSerialized { presentation: IPreparedToolInvocation['presentation']; toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; invocationMessage: string | IMarkdownString; + originMessage: string | IMarkdownString | undefined; pastTenseMessage: string | IMarkdownString | undefined; resultDetails: IToolResult['toolResultDetails']; isConfirmed: boolean | undefined; isComplete: boolean; toolCallId: string; + toolId: string; kind: 'toolInvocationSerialized'; } +export interface IChatExtensionsContent { + extensions: string[]; + kind: 'extensions'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -278,6 +288,7 @@ export type IChatProgress = | IChatConfirmation | IChatToolInvocation | IChatToolInvocationSerialized + | IChatExtensionsContent | IChatUndoStop; export interface IChatFollowup { @@ -374,7 +385,7 @@ export interface IChatEditingSessionAction { kind: 'chatEditingSessionAction'; uri: URI; hasRemainingEdits: boolean; - outcome: 'accepted' | 'rejected' | 'saved'; + outcome: 'accepted' | 'rejected' | 'userModified'; } export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction; @@ -457,6 +468,8 @@ export interface IChatSendRequestOptions { mode?: ChatMode; userSelectedModelId?: string; userSelectedTools?: string[]; + userSelectedTools2?: Record; + toolSelectionIsExclusive?: boolean; location?: ChatAgentLocation; locationData?: IChatLocationData; parserContext?: IChatParserContext; @@ -474,11 +487,6 @@ export interface IChatSendRequestOptions { * The label of the confirmation action that was selected. */ confirmation?: string; - - /** - * Flag to indicate whether a prompt instructions attachment is present. - */ - hasInstructionAttachments?: boolean; } export const IChatService = createDecorator('IChatService'); @@ -521,8 +529,9 @@ export interface IChatService { transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; - readonly unifiedViewEnabled: boolean; - isEditingLocation(location: ChatAgentLocation): boolean; + activateDefaultAgent(location: ChatAgentLocation): Promise; + + readonly edits2Enabled: boolean; } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index f6b2c371941..1c93225df81 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -27,14 +27,14 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; -import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; +import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, isImageVariableEntry, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from './chatParserTypes.js'; import { ChatRequestParser } from './chatRequestParser.js'; import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; -import { IChatVariablesService } from './chatVariables.js'; +import { IChatTransferService } from './chatTransferService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; import { ILanguageModelToolsService } from './languageModelToolsService.js'; @@ -131,16 +131,15 @@ export class ChatService extends Disposable implements IChatService { private readonly _chatServiceTelemetry: ChatServiceTelemetry; private readonly _chatSessionStore: ChatSessionStore; - @memoize - public get unifiedViewEnabled(): boolean { - return !!this.configurationService.getValue(ChatConfiguration.UnifiedChatView); - } - @memoize private get useFileStorage(): boolean { return this.configurationService.getValue(ChatConfiguration.UseFileStorage); } + public get edits2Enabled(): boolean { + return this.configurationService.getValue(ChatConfiguration.Edits2Enabled); + } + private get isEmptyWindow(): boolean { const workspace = this.workspaceContextService.getWorkspace(); return !workspace.configuration && workspace.folders.length === 0; @@ -154,10 +153,10 @@ export class ChatService extends Disposable implements IChatService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, + @IChatTransferService private readonly chatTransferService: IChatTransferService, ) { super(); @@ -201,7 +200,7 @@ export class ChatService extends Disposable implements IChatService { private saveState(): void { const liveChats = Array.from(this._sessionModels.values()) - .filter(session => session.initialLocation === ChatAgentLocation.Panel || session.initialLocation === ChatAgentLocation.EditingSession); + .filter(session => session.initialLocation === ChatAgentLocation.Panel); if (this.useFileStorage) { this._chatSessionStore.storeSessions(liveChats); @@ -216,23 +215,6 @@ export class ChatService extends Disposable implements IChatService { .filter(session => session.requests.length)); allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); - // Only keep one persisted edit session, the latest one. This would be the current one if it's live. - // No way to know whether it's currently live or if it has been cleared and there is no current session. - // But ensure that we don't store multiple edit sessions. - let hasPersistedEditSession = false; - allSessions = allSessions.filter(s => { - if (s.initialLocation === ChatAgentLocation.EditingSession) { - if (hasPersistedEditSession) { - return false; - } else { - hasPersistedEditSession = true; - return true; - } - } - - return true; - }); - allSessions = allSessions.slice(0, maxPersistedSessions); if (allSessions.length) { @@ -306,6 +288,12 @@ export class ChatService extends Disposable implements IChatService { notifyUserAction(action: IChatUserActionEvent): void { this._chatServiceTelemetry.notifyUserAction(action); this._onDidPerformUserAction.fire(action); + if (action.action.kind === 'chatEditingSessionAction') { + const model = this._sessionModels.get(action.sessionId); + if (model) { + model.notifyEditingAction(action.action); + } + } } async setChatSessionTitle(sessionId: string, title: string): Promise { @@ -395,7 +383,7 @@ export class ChatService extends Disposable implements IChatService { async getHistory(): Promise { if (this.useFileStorage) { const liveSessionItems = Array.from(this._sessionModels.values()) - .filter(session => !session.isImported && (session.initialLocation !== ChatAgentLocation.EditingSession || this.unifiedViewEnabled)) + .filter(session => !session.isImported) .map(session => { const title = session.title || localize('newChat', "New Chat"); return { @@ -421,7 +409,7 @@ export class ChatService extends Disposable implements IChatService { .filter(session => !this._sessionModels.has(session.sessionId)); const persistedSessionItems = persistedSessions - .filter(session => !session.isImported && session.initialLocation !== ChatAgentLocation.EditingSession) + .filter(session => !session.isImported) .map(session => { const title = session.customTitle ?? ChatModel.getDefaultTitle(session.requests); return { @@ -432,7 +420,7 @@ export class ChatService extends Disposable implements IChatService { } satisfies IChatDetail; }); const liveSessionItems = Array.from(this._sessionModels.values()) - .filter(session => !session.isImported && session.initialLocation !== ChatAgentLocation.EditingSession) + .filter(session => !session.isImported) .map(session => { const title = session.title || localize('newChat', "New Chat"); return { @@ -476,7 +464,7 @@ export class ChatService extends Disposable implements IChatService { private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, isGlobalEditingSession: boolean, token: CancellationToken): ChatModel { const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, location); - if (location === ChatAgentLocation.EditingSession || (this.unifiedViewEnabled && location === ChatAgentLocation.Panel)) { + if (location === ChatAgentLocation.Panel) { model.startEditingSession(isGlobalEditingSession); } @@ -490,26 +478,10 @@ 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'); - } - - 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. - this.extensionService.activateByEvent(`onChatParticipant:${defaultAgentData.id}`); - } else { - // 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'); - } - } + // Activate the default extension provided agent but do not wait + // for it to be ready so that the session can be used immediately + // without having to wait for the agent to be ready. + this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e)); model.initialize(); } catch (err) { @@ -520,6 +492,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); } @@ -532,10 +521,10 @@ export class ChatService extends Disposable implements IChatService { } let sessionData: ISerializableChatData | undefined; - if (this.useFileStorage) { - sessionData = revive(await this._chatSessionStore.readSession(sessionId)); - } else { + if (!this.useFileStorage || this.transferredSessionData?.sessionId === sessionId) { sessionData = revive(this._persistedSessions[sessionId]); + } else { + sessionData = revive(await this._chatSessionStore.readSession(sessionId)); } if (!sessionData) { @@ -595,7 +584,6 @@ export class ChatService extends Disposable implements IChatService { ...options, locationData: request.locationData, attachedContext: request.attachedContext, - hasInstructionAttachments: options?.hasInstructionAttachments ?? false, }; await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; } @@ -603,13 +591,8 @@ export class ChatService extends Disposable implements IChatService { async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - // if text is not provided, but chat input has `prompt instructions` - // attached, use the default prompt text to avoid empty messages - if (!request.trim() && options?.hasInstructionAttachments) { - request = 'Follow these instructions.'; - } - if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.hasInstructionAttachments) { + if (!request.trim() && !options?.slashCommand && !options?.agentId) { this.trace('sendRequest', 'Rejected empty message'); return; } @@ -751,7 +734,7 @@ export class ChatService extends Disposable implements IChatService { let chatTitlePromise: Promise | undefined; if (agentPart || (defaultAgent && !commandPart)) { - const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): Promise => { + const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => { const initVariableData: IChatRequestVariableData = { variables: [] }; request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId); @@ -761,7 +744,7 @@ export class ChatService extends Disposable implements IChatService { variableData = chatRequest.variableData; message = getPromptText(request.message).message; } else { - variableData = this.chatVariablesService.resolveVariables(parsedRequest, request.attachedContext); + variableData = { variables: this.prepareContext(request.attachedContext) }; model.updateRequest(request, variableData); const promptTextResult = getPromptText(request.message); @@ -784,7 +767,10 @@ export class ChatService extends Disposable implements IChatService { acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, - userSelectedTools: options?.userSelectedTools + userSelectedTools: options?.userSelectedTools, + userSelectedTools2: options?.userSelectedTools2, + toolSelectionIsExclusive: options?.toolSelectionIsExclusive, + editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; @@ -793,7 +779,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); // Prepare the request object that we will send to the participant detection provider - const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); + const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) { @@ -811,7 +797,7 @@ export class ChatService extends Disposable implements IChatService { // Recompute history in case the agent or command changed const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); - const requestProps = await prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); + const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); const pendingRequest = this._pendingRequests.get(sessionId); if (pendingRequest && !pendingRequest.requestId) { pendingRequest.requestId = requestProps.requestId; @@ -822,17 +808,19 @@ export class ChatService extends Disposable implements IChatService { agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined; } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { - request = model.addRequest(parsedRequest, { variables: [] }, attempt); - completeResponseCreated(); + if (commandPart.slashCommand.silent !== true) { + request = model.addRequest(parsedRequest, { variables: [] }, attempt); + completeResponseCreated(); + } // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; - for (const request of model.getRequests()) { - if (!request.response) { + for (const modelRequest of model.getRequests()) { + if (!modelRequest.response) { continue; } - history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: request.message.text }] }); - history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: request.response.response.toString() }] }); + history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] }); + history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] }); } const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { @@ -933,8 +921,29 @@ export class ChatService extends Disposable implements IChatService { }; } + private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] { + attachedContextVariables ??= []; + + // "reverse", high index first so that replacement is simple + attachedContextVariables.sort((a, b) => { + // If either range is undefined, sort it to the back + if (!a.range && !b.range) { + return 0; // Keep relative order if both ranges are undefined + } + if (!a.range) { + return 1; // a goes after b + } + if (!b.range) { + return -1; // a goes before b + } + return b.range.start - a.range.start; + }); + + return attachedContextVariables; + } + private async checkAgentAllowed(agent: IChatAgentData): Promise { - if (agent.isToolsAgent) { + if (agent.modes.includes(ChatMode.Agent)) { const enabled = await this.experimentService.getTreatment('chatAgentEnabled'); if (enabled === false) { throw new Error('Agent is currently disabled'); @@ -949,7 +958,7 @@ export class ChatService extends Disposable implements IChatService { return 'implicit'; } else if (v.range) { // 'range' is range within the prompt text - if (v.isTool) { + if (v.kind === 'tool') { return 'toolInPrompt'; } else { return 'fileInPrompt'; @@ -958,11 +967,11 @@ export class ChatService extends Disposable implements IChatService { return 'command'; } else if (v.kind === 'symbol') { return 'symbol'; - } else if (v.isImage) { + } else if (isImageVariableEntry(v)) { return 'image'; - } else if (v.isDirectory) { + } else if (v.kind === 'directory') { return 'directory'; - } else if (v.isTool) { + } else if (v.kind === 'tool') { return 'tool'; } else { if (URI.isUri(v.value)) { @@ -998,7 +1007,8 @@ export class ChatService extends Disposable implements IChatService { message: promptTextResult.message, command: request.response.slashCommand?.name, variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack - location: ChatAgentLocation.Panel + location: ChatAgentLocation.Panel, + editedFileEvents: request.editedFileEvents, }; history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} }); } @@ -1138,13 +1148,10 @@ 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()}`); } - isEditingLocation(location: ChatAgentLocation): boolean { - return location === ChatAgentLocation.EditingSession || this.unifiedViewEnabled; - } - getChatStorageFolder(): URI { return this._chatSessionStore.getChatStorageFolder(); } diff --git a/code/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts b/code/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts index 1da03b0d8e0..68480c1dddb 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts @@ -24,6 +24,16 @@ export interface IChatSlashData { * as it is entered. Defaults to `false`. */ executeImmediately?: boolean; + + /** + * Whether the command should be added as a request/response + * turn to the chat history. Defaults to `false`. + * + * For instance, the `/save` command opens an untitled document + * to the side hence does not contain any chatbot responses. + */ + silent?: boolean; + locations: ChatAgentLocation[]; modes?: ChatMode[]; } diff --git a/code/src/vs/workbench/contrib/chat/common/chatTransferService.ts b/code/src/vs/workbench/contrib/chat/common/chatTransferService.ts index 22a2eb31aa8..bbc21070343 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatTransferService.ts +++ b/code/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/code/src/vs/workbench/contrib/chat/common/chatVariables.ts b/code/src/vs/workbench/contrib/chat/common/chatVariables.ts index 809d5637c53..a5f936c7360 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -9,10 +9,9 @@ import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { Location } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js'; -import { IParsedChatRequest } from './chatParserTypes.js'; +import { IChatModel, IDiagnosticVariableEntryFilterData } from './chatModel.js'; import { IChatContentReference, IChatProgressMessage } from './chatService.js'; -import { ChatAgentLocation } from './constants.js'; +import { IToolData } from './languageModelToolsService.js'; export interface IChatVariableData { id: string; @@ -47,12 +46,7 @@ export const IChatVariablesService = createDecorator('ICh export interface IChatVariablesService { _serviceBrand: undefined; getDynamicVariables(sessionId: string): ReadonlyArray; - attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; - - /** - * Resolves all variables that occur in `prompt` - */ - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData; + getSelectedTools(sessionId: string): ReadonlyArray; } export interface IDynamicVariable { @@ -60,7 +54,6 @@ export interface IDynamicVariable { id: string; fullName?: string; icon?: ThemeIcon; - prefix?: string; modelDescription?: string; isFile?: boolean; isDirectory?: boolean; diff --git a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts index a152eb57eb8..cab2fd082c2 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -137,6 +137,7 @@ export interface IChatReferences { export interface IChatWorkingProgress { kind: 'working'; isPaused: boolean; + setPaused(paused: boolean): void; } /** diff --git a/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index 4522c0b7bd3..865425b70e2 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/code/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/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 7ce74db646a..1099dca6e39 100644 --- a/code/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/code/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('<')) { + // ; + export type ToolDataSource = | { type: 'extension'; + label: string; extensionId: ExtensionIdentifier; /** * True for tools contributed through extension API from third-party extensions, so they can be disabled by policy. @@ -50,7 +60,12 @@ export type ToolDataSource = */ isExternalTool: boolean; } - | { type: 'mcp'; collectionId: string; definitionId: string } + | { + type: 'mcp'; + label: string; + collectionId: string; + definitionId: string; + } | { type: 'internal' }; export namespace ToolDataSource { @@ -85,17 +100,23 @@ export function isToolInvocationContext(obj: any): obj is IToolInvocationContext export interface IToolResultInputOutputDetails { readonly input: string; - readonly output: string; + readonly output: ({ type: 'text'; value: string } | { type: 'data'; mimeType: string; value64: string })[]; + readonly isError?: boolean; } export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails { - return typeof obj === 'object' && typeof obj?.input === 'string' && typeof obj?.output === 'string'; + return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output)); } export interface IToolResult { - content: (IToolResultPromptTsxPart | IToolResultTextPart)[]; + content: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[]; toolResultMessage?: string | IMarkdownString; toolResultDetails?: Array | IToolResultInputOutputDetails; + toolResultError?: string; +} + +export function toolResultHasBuffers(result: IToolResult): boolean { + return result.content.some(part => part.kind === 'data'); } export interface IToolResultPromptTsxPart { @@ -112,6 +133,14 @@ export interface IToolResultTextPart { value: string; } +export interface IToolResultDataPart { + kind: 'data'; + value: { + mimeType: string; + data: VSBuffer; + }; +} + export interface IToolConfirmationMessages { title: string; message: string | IMarkdownString; @@ -121,13 +150,15 @@ export interface IToolConfirmationMessages { export interface IPreparedToolInvocation { invocationMessage?: string | IMarkdownString; pastTenseMessage?: string | IMarkdownString; + originMessage?: string | IMarkdownString; confirmationMessages?: IToolConfirmationMessages; presentation?: 'hidden' | undefined; + // When this gets extended, be sure to update `chatResponseAccessibleView.ts` to handle the new properties. toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; } export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; prepareToolInvocation?(parameters: any, token: CancellationToken): Promise; } @@ -155,3 +186,10 @@ export function createToolInputUri(toolOrId: IToolData | string): URI { } return URI.from({ scheme: Schemas.inMemory, path: `/lm/tool/${toolOrId}/tool_input.json` }); } + +export function createToolSchemaUri(toolOrId: IToolData | string): URI { + if (typeof toolOrId !== 'string') { + toolOrId = toolOrId.id; + } + return URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolOrId}` }); +} diff --git a/code/src/vs/workbench/contrib/chat/common/languageModels.ts b/code/src/vs/workbench/contrib/chat/common/languageModels.ts index ba03b133a1a..62ed7dcdf82 100644 --- a/code/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/code/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -15,6 +15,7 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +// import { ChatImagePart } from '../../../api/common/extHostTypes.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js'; import { ChatContextKeys } from './chatContextKeys.js'; @@ -35,6 +36,12 @@ export interface IChatMessageImagePart { value: IChatImageURLPart; } +export interface IChatMessageExtraDataPart { + type: 'extra_data'; + kind: string; + data: any; +} + export interface IChatImageURLPart { /** * The image's MIME type (e.g., "image/png", "image/jpeg"). @@ -70,11 +77,11 @@ export enum ImageDetailLevel { export interface IChatMessageToolResultPart { type: 'tool_result'; toolCallId: string; - value: (IChatResponseTextPart | IChatResponsePromptTsxPart)[]; + value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[]; isError?: boolean; } -export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart; +export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageExtraDataPart; export interface IChatMessage { readonly name?: string | undefined; @@ -92,6 +99,11 @@ export interface IChatResponsePromptTsxPart { value: unknown; } +export interface IChatResponseDataPart { + type: 'data'; + value: IChatImageURLPart; +} + export interface IChatResponseToolUsePart { type: 'tool_use'; name: string; @@ -99,7 +111,7 @@ export interface IChatResponseToolUsePart { parameters: any; } -export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart; +export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart; export interface IChatResponseFragment { index: number; @@ -113,6 +125,8 @@ export interface ILanguageModelChatMetadata { readonly id: string; readonly vendor: string; readonly version: string; + readonly description?: string; + readonly cost?: string; readonly family: string; readonly maxInputTokens: number; readonly maxOutputTokens: number; @@ -120,6 +134,7 @@ export interface ILanguageModelChatMetadata { readonly isDefault?: boolean; readonly isUserSelectable?: boolean; + readonly modelPickerCategory: { label: string; order: number }; readonly auth?: { readonly providerLabel: string; readonly accountLabel?: string; diff --git a/code/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts b/code/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts new file mode 100644 index 00000000000..779c011f797 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../nls.js'; + +export const DEFAULT_MODEL_PICKER_CATEGORY = { label: localize('chat.modelPicker.other', "Other Models"), order: Number.MAX_SAFE_INTEGER }; diff --git a/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index e19588df4e6..a190b50d6c2 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -121,7 +121,7 @@ export class RecursiveReference extends ResolveError { constructor( uri: URI, - public readonly recursivePath: string[], + public readonly recursivePath: readonly string[], ) { // sanity check - a recursive path must always have at least // two items in the list, otherwise it is not a recursive loop diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts index 7522f5acd0b..7a48fc44494 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts @@ -11,15 +11,15 @@ import { ICodec } from '../../../../../../base/common/codecs/types/ICodec.js'; /** * `ChatPromptCodec` type is a `ICodec` with specific types for * stream messages and return types of the `encode`/`decode` functions. - * @see {@linkcode ICodec} + * @see {@link ICodec} */ interface IChatPromptCodec extends ICodec { /** * Decode a stream of `VSBuffer`s into a stream of `TChatPromptToken`s. * - * @see {@linkcode TChatPromptToken} - * @see {@linkcode VSBuffer} - * @see {@linkcode ChatPromptDecoder} + * @see {@link TChatPromptToken} + * @see {@link VSBuffer} + * @see {@link ChatPromptDecoder} */ decode: (value: ReadableStream) => ChatPromptDecoder; } @@ -31,8 +31,8 @@ export const ChatPromptCodec: IChatPromptCodec = Object.freeze({ /** * Encode a stream of `TChatPromptToken`s into a stream of `VSBuffer`s. * - * @see {@linkcode ReadableStream} - * @see {@linkcode VSBuffer} + * @see {@link ReadableStream} + * @see {@link VSBuffer} */ encode: (_stream: ReadableStream): ReadableStream => { throw new Error('The `encode` method is not implemented.'); @@ -41,10 +41,10 @@ export const ChatPromptCodec: IChatPromptCodec = Object.freeze({ /** * Decode a of `VSBuffer`s into a readable of `TChatPromptToken`s. * - * @see {@linkcode TChatPromptToken} - * @see {@linkcode VSBuffer} - * @see {@linkcode ChatPromptDecoder} - * @see {@linkcode ReadableStream} + * @see {@link TChatPromptToken} + * @see {@link VSBuffer} + * @see {@link ChatPromptDecoder} + * @see {@link ReadableStream} */ decode: (stream: ReadableStream): ChatPromptDecoder => { return new ChatPromptDecoder(stream); diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts index b1bd3de2b92..6aba6b0ead9 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts @@ -4,20 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import { PromptToken } from './tokens/promptToken.js'; +import { PromptAtMention } from './tokens/promptAtMention.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { PromptSlashCommand } from './tokens/promptSlashCommand.js'; import { assertNever } from '../../../../../../base/common/assert.js'; import { ReadableStream } from '../../../../../../base/common/stream.js'; +import { PartialPromptAtMention } from './parsers/promptAtMentionParser.js'; +import { PromptTemplateVariable } from './tokens/promptTemplateVariable.js'; +import { PartialPromptSlashCommand } from './parsers/promptSlashCommandParser.js'; import { BaseDecoder } from '../../../../../../base/common/codecs/baseDecoder.js'; import { PromptVariable, PromptVariableWithData } from './tokens/promptVariable.js'; +import { At } from '../../../../../../editor/common/codecs/simpleCodec/tokens/at.js'; import { Hash } from '../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; -import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { Slash } from '../../../../../../editor/common/codecs/simpleCodec/tokens/slash.js'; +import { DollarSign } from '../../../../../../editor/common/codecs/simpleCodec/tokens/dollarSign.js'; import { PartialPromptVariableName, PartialPromptVariableWithData } from './parsers/promptVariableParser.js'; import { MarkdownDecoder, TMarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/markdownDecoder.js'; +import { PartialPromptTemplateVariable, PartialPromptTemplateVariableStart, TPromptTemplateVariableParser } from './parsers/promptTemplateVariableParser.js'; /** * Tokens produced by this decoder. */ -export type TChatPromptToken = MarkdownLink | PromptVariable | PromptVariableWithData; +export type TChatPromptToken = TMarkdownToken | (PromptVariable | PromptVariableWithData) + | PromptAtMention | PromptSlashCommand | PromptTemplateVariable; /** * Decoder for the common chatbot prompt message syntax. @@ -29,7 +38,9 @@ export class ChatPromptDecoder extends BaseDecoder, @@ -38,29 +49,46 @@ export class ChatPromptDecoder extends BaseDecoder { return token.symbol; }); + +/** + * List of characters that cannot be in an at-mention name (excluding the {@link STOP_CHARACTERS}). + */ +export const INVALID_NAME_CHARACTERS: readonly string[] = [ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `prompt @mention` sequences. + * E.g., `@workspace` or `@github` participant mention. + */ +export class PartialPromptAtMention extends ParserBase { + constructor(token: At) { + super([token]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + // if a `stop` character is encountered, finish the parsing process + if (STOP_CHARACTERS.includes(token.text)) { + try { + // if it is possible to convert current parser to `PromptAtMention`, return success result + return { + result: 'success', + nextParser: this.asPromptAtMention(), + wasTokenConsumed: false, + }; + } catch (error) { + // otherwise fail + return { + result: 'failure', + wasTokenConsumed: false, + }; + } finally { + // in any case this is an end of the parsing process + this.isConsumed = true; + } + } + + // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names + if (INVALID_NAME_CHARACTERS.includes(token.text)) { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // otherwise it is a valid name character, so add it to the list of + // the current tokens and continue the parsing process + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Try to convert current parser instance into a fully-parsed {@link PromptAtMention} token. + * + * @throws if sequence of tokens received so far do not constitute a valid prompt variable, + * for instance, if there is only `1` starting `@` token is available. + */ + public asPromptAtMention(): PromptAtMention { + // if there is only one token before the stop character + // must be the starting `@` one), then fail + assert( + this.currentTokens.length > 1, + 'Cannot create a prompt @mention out of incomplete token sequence.', + ); + + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + // render the characters above into strings, excluding the starting `@` character + const nameTokens = this.currentTokens.slice(1); + const atMentionName = nameTokens.map(pick('text')).join(''); + + return new PromptAtMention( + new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ), + atMentionName, + ); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts new file mode 100644 index 00000000000..159ffd54bc8 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { pick } from '../../../../../../../base/common/arrays.js'; +import { assert } from '../../../../../../../base/common/assert.js'; +import { PromptSlashCommand } from '../tokens/promptSlashCommand.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { At } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/at.js'; +import { Tab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/tab.js'; +import { Hash } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; +import { Slash } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/slash.js'; +import { Space } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/space.js'; +import { Colon } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/colon.js'; +import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { FormFeed } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/formFeed.js'; +import { VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/verticalTab.js'; +import { TSimpleDecoderToken } from '../../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; +import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; +import { ExclamationMark } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/exclamationMark.js'; +import { LeftBracket, RightBracket } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/brackets.js'; +import { LeftAngleBracket, RightAngleBracket } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/angleBrackets.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../../../../../../editor/common/codecs/simpleCodec/parserBase.js'; + +/** + * List of characters that terminate the prompt at-mention sequence. + */ +export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed, Colon, At, Hash, Slash] + .map((token) => { return token.symbol; }); + +/** + * List of characters that cannot be in an at-mention name (excluding the {@link STOP_CHARACTERS}). + */ +export const INVALID_NAME_CHARACTERS: readonly string[] = [ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `prompt /command` sequences. + * E.g., `/search` or `/explain` command. + */ +export class PartialPromptSlashCommand extends ParserBase { + constructor(token: Slash) { + super([token]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + // if a `stop` character is encountered, finish the parsing process + if (STOP_CHARACTERS.includes(token.text)) { + try { + // if it is possible to convert current parser to `PromptSlashCommand`, return success result + return { + result: 'success', + nextParser: this.asPromptSlashCommand(), + wasTokenConsumed: false, + }; + } catch (error) { + // otherwise fail + return { + result: 'failure', + wasTokenConsumed: false, + }; + } finally { + // in any case this is an end of the parsing process + this.isConsumed = true; + } + } + + // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names + if (INVALID_NAME_CHARACTERS.includes(token.text)) { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // otherwise it is a valid name character, so add it to the list of + // the current tokens and continue the parsing process + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Try to convert current parser instance into a fully-parsed {@link PromptSlashCommand} token. + * + * @throws if sequence of tokens received so far do not constitute a valid prompt variable, + * for instance, if there is only `1` starting `/` token is available. + */ + public asPromptSlashCommand(): PromptSlashCommand { + // if there is only one token before the stop character + // must be the starting `/` one), then fail + assert( + this.currentTokens.length > 1, + 'Cannot create a prompt /command out of incomplete token sequence.', + ); + + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + // render the characters above into strings, excluding the starting `/` character + const nameTokens = this.currentTokens.slice(1); + const atMentionName = nameTokens.map(pick('text')).join(''); + + return new PromptSlashCommand( + new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ), + atMentionName, + ); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts new file mode 100644 index 00000000000..c40a8c5622f --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../../base/common/assert.js'; +import { PromptTemplateVariable } from '../tokens/promptTemplateVariable.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { TSimpleDecoderToken } from '../../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; +import { DollarSign, LeftCurlyBrace, RightCurlyBrace } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../../../../../../editor/common/codecs/simpleCodec/parserBase.js'; + +/** + * Parsers of the `${variable}` token sequence in a prompt text. + */ +export type TPromptTemplateVariableParser = PartialPromptTemplateVariableStart | PartialPromptTemplateVariable; + +/** + * Parser that handles start sequence of a `${variable}` token sequence in + * a prompt text. Transitions to {@link PartialPromptTemplateVariable} parser + * as soon as the `${` character sequence is found. + */ +export class PartialPromptTemplateVariableStart extends ParserBase { + constructor(token: DollarSign) { + super([token]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (token instanceof LeftCurlyBrace) { + this.currentTokens.push(token); + + this.isConsumed = true; + return { + result: 'success', + nextParser: new PartialPromptTemplateVariable(this.currentTokens), + wasTokenConsumed: true, + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * Parser that handles a partial `${variable}` token sequence in a prompt text. + */ +export class PartialPromptTemplateVariable extends ParserBase { + constructor(tokens: (DollarSign | LeftCurlyBrace)[]) { + super(tokens); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + // template variables are terminated by the `}` character + if (token instanceof RightCurlyBrace) { + this.currentTokens.push(token); + + this.isConsumed = true; + return { + result: 'success', + nextParser: this.asPromptTemplateVariable(), + wasTokenConsumed: true, + }; + } + + // otherwise it is a valid name character, so add it to the list of + // the current tokens and continue the parsing process + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Returns a string representation of the prompt template variable + * contents, if any is present. + */ + private get contents(): string { + const contentTokens: TSimpleDecoderToken[] = []; + + // template variables are surrounded by `${}`, hence we need to have + // at least `${` plus one character for the contents to be non-empty + if (this.currentTokens.length < 3) { + return ''; + } + + // collect all tokens besides the first two (`${`) and a possible `}` at the end + for (let i = 2; i < this.currentTokens.length; i++) { + const token = this.currentTokens[i]; + const isLastToken = (i === this.currentTokens.length - 1); + + if ((token instanceof RightCurlyBrace) && (isLastToken === true)) { + break; + } + + contentTokens.push(token); + } + + return BaseToken.render(contentTokens); + } + + /** + * Try to convert current parser instance into a {@link PromptTemplateVariable} token. + * + * @throws if: + * - current tokens sequence cannot be converted to a valid template variable token + */ + public asPromptTemplateVariable(): PromptTemplateVariable { + const firstToken = this.currentTokens[0]; + const secondToken = this.currentTokens[1]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + // template variables are surrounded by `${}`, hence we need + // to have at least 3 tokens in the list for a valid one + assert( + this.currentTokens.length >= 3, + 'Prompt template variable should have at least 3 tokens.', + ); + + // a complete template variable must end with a `}` + assert( + lastToken instanceof RightCurlyBrace, + 'Last token is not a "}".', + ); + + // sanity checks of the first and second tokens + assert( + firstToken instanceof DollarSign, + 'First token must be a "$".', + ); + assert( + secondToken instanceof LeftCurlyBrace, + 'Second token must be a "{".', + ); + + return new PromptTemplateVariable( + BaseToken.fullRange(this.currentTokens), + this.contents, + ); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts index d7fddaf9a06..376f251b2ff 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { pick } from '../../../../../../../base/common/arrays.js'; import { assert } from '../../../../../../../base/common/assert.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; import { PromptVariable, PromptVariableWithData } from '../tokens/promptVariable.js'; +import { At } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/at.js'; import { Tab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/tab.js'; import { Hash } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; import { Space } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/space.js'; import { Colon } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/colon.js'; import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; import { FormFeed } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/formFeed.js'; -import { TSimpleToken } from '../../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; import { VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/verticalTab.js'; +import { TSimpleDecoderToken } from '../../../../../../../editor/common/codecs/simpleCodec/simpleDecoder.js'; import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; import { ExclamationMark } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/exclamationMark.js'; import { LeftBracket, RightBracket } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/brackets.js'; @@ -24,7 +25,7 @@ import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../../../. /** * List of characters that terminate the prompt variable sequence. */ -export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed] +export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed, Hash, At] .map((token) => { return token.symbol; }); /** @@ -35,18 +36,18 @@ export const INVALID_NAME_CHARACTERS: readonly string[] = [Hash, Colon, Exclamat /** * The parser responsible for parsing a `prompt variable name`. - * E.g., `#selection` or `#workspace` variable. If the `:` character follows + * E.g., `#selection` or `#codebase` variable. If the `:` character follows * the variable name, the parser transitions to {@link PartialPromptVariableWithData} * that is also able to parse the `data` part of the variable. E.g., the `#file` part * of the `#file:/path/to/something.md` sequence. */ -export class PartialPromptVariableName extends ParserBase { +export class PartialPromptVariableName extends ParserBase { constructor(token: Hash) { super([token]); } @assertNotConsumed - public accept(token: TSimpleToken): TAcceptTokenResult { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // if a `stop` character is encountered, finish the parsing process if (STOP_CHARACTERS.includes(token.text)) { try { @@ -130,7 +131,7 @@ export class PartialPromptVariableName extends ParserBase { +export class PartialPromptVariableWithData extends ParserBase { - constructor(tokens: readonly TSimpleToken[]) { + constructor(tokens: readonly TSimpleDecoderToken[]) { const firstToken = tokens[0]; const lastToken = tokens[tokens.length - 1]; @@ -172,7 +173,7 @@ export class PartialPromptVariableWithData extends ParserBase { + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { // if a `stop` character is encountered, finish the parsing process if (STOP_CHARACTERS.includes(token.text)) { // in any case, success of failure below, this is an end of the parsing process @@ -195,8 +196,8 @@ export class PartialPromptVariableWithData extends ParserBase(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if ((other instanceof PromptAtMention) === false) { + return false; + } + + if (this.text.length !== other.text.length) { + return false; + } + + return this.text === other.text; + } + + /** + * Return a string representation of the token. + */ + public override toString(): string { + return `${this.text}${this.range}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts new file mode 100644 index 00000000000..7b1e39b033d --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptToken } from './promptToken.js'; +import { assert } from '../../../../../../../base/common/assert.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { INVALID_NAME_CHARACTERS, STOP_CHARACTERS } from '../parsers/promptSlashCommandParser.js'; + +/** + * All prompt at-mentions start with `/` character. + */ +const START_CHARACTER: string = '/'; + +/** + * Represents a `/command` token in a prompt text. + */ +export class PromptSlashCommand extends PromptToken { + constructor( + range: Range, + /** + * The name of a command, excluding the `/` character at the start. + */ + public readonly name: string, + ) { + // sanity check of characters used in the provided command name + for (const character of name) { + assert( + (INVALID_NAME_CHARACTERS.includes(character) === false) && + (STOP_CHARACTERS.includes(character) === false), + `Slash command 'name' cannot contain character '${character}', got '${name}'.`, + ); + } + + super(range); + } + + /** + * Get full text of the token. + */ + public get text(): string { + return `${START_CHARACTER}${this.name}`; + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if ((other instanceof PromptSlashCommand) === false) { + return false; + } + + if (this.text.length !== other.text.length) { + return false; + } + + return this.text === other.text; + } + + /** + * Return a string representation of the token. + */ + public override toString(): string { + return `${this.text}${this.range}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts new file mode 100644 index 00000000000..31499f1d333 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptToken } from './promptToken.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { DollarSign } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/dollarSign.js'; +import { LeftCurlyBrace, RightCurlyBrace } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/curlyBraces.js'; + +/** + * Represents a `${variable}` token in a prompt text. + */ +export class PromptTemplateVariable extends PromptToken { + constructor( + range: Range, + /** + * The contents of the template variable, excluding + * the surrounding `${}` characters. + */ + public readonly contents: string, + ) { + super(range); + } + + /** + * Get full text of the token. + */ + public get text(): string { + return [ + DollarSign.symbol, + LeftCurlyBrace.symbol, + this.contents, + RightCurlyBrace.symbol, + ].join(''); + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if ((other instanceof PromptTemplateVariable) === false) { + return false; + } + + if (this.text.length !== other.text.length) { + return false; + } + + return this.text === other.text; + } + + /** + * Return a string representation of the token. + */ + public override toString(): string { + return `${this.text}${this.range}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts index 7d5d89d8ca3..4dd0c536640 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts @@ -3,33 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageFilter } from '../../../../../editor/common/languageSelector.js'; -import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js'; +import { LanguageSelector } from '../../../../../editor/common/languageSelector.js'; /** * Documentation link for the reusable prompts feature. */ -export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; +export const PROMPT_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; +export const INSTRUCTIONS_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-custom-instructions'; /** - * Supported reusable prompt file patterns. + * Language ID for the reusable prompt syntax. */ -const REUSABLE_PROMPT_FILE_PATTERNS = Object.freeze([ - /** - * Any file that has the prompt file extension. - * See {@link PROMPT_FILE_EXTENSION}. - */ - `**/*${PROMPT_FILE_EXTENSION}`, +export const PROMPT_LANGUAGE_ID = 'prompt'; - /** - * Copilot custom instructions file inside a `.github` folder. - */ - `**/.github/${COPILOT_CUSTOM_INSTRUCTIONS_FILENAME}`, -]); +/** + * Language ID for instructions syntax. + */ +export const INSTRUCTIONS_LANGUAGE_ID = 'instructions'; /** - * Prompt files language selector. + * Prompt and instructions files language selector. */ -export const LANGUAGE_SELECTOR: LanguageFilter = Object.freeze({ - pattern: `{${REUSABLE_PROMPT_FILE_PATTERNS.join(',')}}`, -}); +export const PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR: LanguageSelector = [PROMPT_LANGUAGE_ID, INSTRUCTIONS_LANGUAGE_ID]; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index 87bf7504054..723bf10ea3f 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -3,27 +3,55 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { PROMPT_LANGUAGE_ID } from '../constants.js'; import { IPromptContentsProvider } from './types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; -import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { IPromptContentsProviderOptions, PromptContentsProviderBase } from './promptContentsProviderBase.js'; +import { isPromptOrInstructionsFile } from '../../../../../../platform/prompts/common/constants.js'; import { OpenFailed, NotPromptFile, ResolveError, FolderReference } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; /** - * Prompt contents provider for a file on the disk referenced by the provided {@linkcode URI}. + * Prompt contents provider for a file on the disk referenced by + * a provided {@link URI}. */ export class FilePromptContentProvider extends PromptContentsProviderBase implements IPromptContentsProvider { + public override get sourceName(): string { + return 'file'; + } + + public override get languageId(): string { + const model = this.modelService.getModel(this.uri); + + if (model !== null) { + return model.getLanguageId(); + } + + const inferredId = this.languageService + .guessLanguageIdByFilepathOrFirstLine(this.uri); + + if (inferredId !== null) { + return inferredId; + } + + // fallback to the default prompt language ID + return PROMPT_LANGUAGE_ID; + } + constructor( public readonly uri: URI, + options: Partial = {}, @IFileService private readonly fileService: IFileService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, ) { - super(); + super(options); // make sure the object is updated on file changes this._register( @@ -80,42 +108,44 @@ export class FilePromptContentProvider extends PromptContentsProviderBase = {}, ): IPromptContentsProvider { return new FilePromptContentProvider( promptContentsSource.uri, + options, this.fileService, + this.modelService, + this.languageService, ); } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts index 4f1a32fd5c2..e90b3036aed 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts @@ -14,18 +14,36 @@ import { ObservableDisposable } from '../../../../../../base/common/observableDi import { FailedToResolveContentsStream, ResolveError } from '../../promptFileReferenceErrors.js'; import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; +/** + * Options of the {@link PromptContentsProviderBase} class. + */ +export interface IPromptContentsProviderOptions { + /** + * Whether to allow files that don't have usual prompt + * file extension to be treated as a prompt file. + */ + readonly allowNonPromptFiles: boolean; +} + +/** + * Default {@link IPromptContentsProviderOptions} options. + */ +export const DEFAULT_OPTIONS: IPromptContentsProviderOptions = { + allowNonPromptFiles: false, +}; + /** * Base class for prompt contents providers. Classes that extend this one are responsible to: * - * - implement the {@linkcode getContentsStream} method to provide the contents stream + * - implement the {@link getContentsStream} method to provide the contents stream * of a prompt; this method should throw a `ResolveError` or its derivative if the contents * cannot be parsed for any reason - * - fire a {@linkcode TChangeEvent} event on the {@linkcode onChangeEmitter} event when + * - fire a {@link TChangeEvent} event on the {@link onChangeEmitter} event when * prompt contents change * - misc: - * - provide the {@linkcode uri} property that represents the URI of a prompt that + * - provide the {@link uri} property that represents the URI of a prompt that * the contents are for - * - implement the {@linkcode toString} method to return a string representation of this + * - implement the {@link toString} method to return a string representation of this * provider type to aid with debugging/tracing */ export abstract class PromptContentsProviderBase< @@ -34,6 +52,8 @@ export abstract class PromptContentsProviderBase< public abstract readonly uri: URI; public abstract createNew(promptContentsSource: { uri: URI }): IPromptContentsProvider; public abstract override toString(): string; + public abstract get languageId(): string; + public abstract get sourceName(): string; /** * Function to get contents stream for the provider. This function should @@ -55,17 +75,29 @@ export abstract class PromptContentsProviderBase< */ protected readonly onChangeEmitter = this._register(new Emitter()); - constructor() { + /** + * Options passed to the constructor, extended with + * value defaults from {@link DEFAULT_OPTIONS}. + */ + protected readonly options: IPromptContentsProviderOptions; + + constructor( + options: Partial, + ) { super(); + + this.options = { + ...DEFAULT_OPTIONS, + ...options, + }; + // ensure that the `onChangeEmitter` always fires with the correct context this.onChangeEmitter.fire = this.onChangeEmitter.fire.bind(this.onChangeEmitter); - // subscribe to the change event emitted by an extending class - this._register(this.onChangeEmitter.event(this.onContentsChanged, this)); } /** * Event emitter for the prompt contents change event. - * See {@linkcode onContentChanged} for more details. + * See {@link onContentChanged} for more details. */ private readonly onContentChangedEmitter = this._register(new Emitter()); @@ -76,7 +108,7 @@ export abstract class PromptContentsProviderBase< * * `Note!` this field is meant to be used by the external consumers of the prompt * contents provider that the classes that extend this abstract class. - * Please use the {@linkcode onChangeEmitter} event to provide a change + * Please use the {@link onChangeEmitter} event to provide a change * event in your prompt contents implementation instead. */ public readonly onContentChanged = this.onContentChangedEmitter.event; @@ -130,6 +162,9 @@ export abstract class PromptContentsProviderBase< // `'full'` means "everything has changed" this.onContentsChanged('full'); + // subscribe to the change event emitted by a child class + this._register(this.onChangeEmitter.event(this.onContentsChanged, this)); + return this; } } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts index 71907911652..75f0623ff3b 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts @@ -7,14 +7,15 @@ import { IPromptContentsProvider } from './types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { FilePromptContentProvider } from './filePromptContentsProvider.js'; -import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; +import { TextModel } from '../../../../../../editor/common/model/textModel.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { newWriteableStream, ReadableStream } from '../../../../../../base/common/stream.js'; import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; +import { IPromptContentsProviderOptions, PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { TextModel } from '../../../../../../editor/common/model/textModel.js'; /** * Prompt contents provider for a {@link ITextModel} instance. @@ -23,13 +24,25 @@ export class TextModelContentsProvider extends PromptContentsProviderBase = {}, @IInstantiationService private readonly initService: IInstantiationService, + @ILogService private readonly logService: ILogService, ) { - super(); + super(options); this._register(this.model.onWillDispose(this.dispose.bind(this))); this._register(this.model.onDidChangeContent(this.onChangeEmitter.fire)); @@ -51,11 +64,21 @@ export class TextModelContentsProvider extends PromptContentsProviderBase> { const stream = newWriteableStream(null); - const linesCount = this.model.getLineCount(); + + // the `getLineCount`method throws is model is already disposed + // hence to be extra safe, we check the model state before getting + // the number of available lines in the text model + if (this.model.isDisposed()) { + stream.end(); + stream.destroy(); + + return stream; + } // provide the changed lines to the stream incrementally and asynchronously // to avoid blocking the main thread and save system resources used let i = 1; + const linesCount = this.model.getLineCount(); const interval = setInterval(() => { // if we have written all lines or lines count is zero, // end the stream and stop the interval timer @@ -88,7 +111,13 @@ export class TextModelContentsProvider extends PromptContentsProviderBase = {}, ): IPromptContentsProvider { if (promptContentsSource instanceof TextModel) { return this.initService.createInstance( TextModelContentsProvider, promptContentsSource, + options, ); } return this.initService.createInstance( FilePromptContentProvider, promptContentsSource.uri, + options, ); } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts similarity index 91% rename from code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts index 71a659c4d4f..3b32a4cf7b8 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts @@ -19,6 +19,16 @@ export interface IPromptContentsProvider extends IDisposable { */ readonly uri: URI; + /** + * Language ID of the prompt contents. + */ + readonly languageId: string; + + /** + * Prompt contents source name. + */ + readonly sourceName: string; + /** * Start the contents provider to produce the underlying contents. */ diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts new file mode 100644 index 00000000000..1baac3ffc4d --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../base/common/assert.js'; +import { asBoolean } from '../../../../../../platform/prompts/common/config.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { CONFIG_KEY, PROMPT_LOCATIONS_CONFIG_KEY } from '../../../../../../platform/prompts/common/constants.js'; + +/** + * Contribution that migrates the old config setting value to a new one. + * + * Note! This is a temporary logic and can be removed on ~2026-04-29. + */ +export class ConfigMigration implements IWorkbenchContribution { + constructor( + @IConfigurationService configService: IConfigurationService, + ) { + const value = configService.getValue(CONFIG_KEY); + + // if setting is not set, nothing to do + if ((value === undefined) || (value === null)) { + return; + } + + // if the setting value is a boolean, we don't need to do + // anything since it is already a valid configuration value + if ((typeof value === 'boolean') || (asBoolean(value) !== undefined)) { + return; + } + + // in the old setting logic an array of strings was treated + // as a list of locations, so we need to migrate that + if (Array.isArray(value)) { + + // copy array values into a map of paths + const locationsValue: Record = {}; + for (const filePath of value) { + if (typeof filePath !== 'string') { + continue; + } + const trimmedValue = filePath.trim(); + if (!trimmedValue) { + continue; + } + + locationsValue[trimmedValue] = true; + } + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, locationsValue); + return; + } + + // in the old setting logic an object was treated as a map + // of `location -> boolean`, so we need to migrate that + if (typeof value === 'object') { + // sanity check on the contents of value variable - while + // we've handled the 'null' case above this assertion is + // here to prevent churn when this block is moved around + assert( + value !== null, + 'Object value must not be a null.', + ); + + // copy object values into a map of paths + const locationsValue: Record = {}; + for (const [location, enabled] of Object.entries(value)) { + // if the old location enabled value wasn't a boolean + // then ignore it as it is not a valid value + if ((typeof enabled !== 'boolean') || (asBoolean(enabled) === undefined)) { + continue; + } + + const trimmedValue = location.trim(); + if (!trimmedValue) { + continue; + } + + locationsValue[trimmedValue] = enabled; + } + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, locationsValue); + + return; + } + + // in the old setting logic a string was treated as a single + // location path, so we need to migrate that + if (typeof value === 'string') { + // sanity check on the contents of value variable - while + // we've handled the 'boolean' case above this assertion is + // here to prevent churn when this block is moved around + assert( + asBoolean(value) === undefined, + `String value must not be a boolean, got '${value}'.`, + ); + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, { [value]: true }); + return; + } + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.ts new file mode 100644 index 00000000000..2eebfc02899 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConfigMigration } from './configMigration.js'; +import { LANGUAGE_FEATURE_CONTRIBUTIONS } from './languageFeatures/index.js'; +import { Registry } from '../../../../../../platform/registry/common/platform.js'; +import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../../common/contributions.js'; + +/** + * Function that registers all prompt-file related contributions. + */ +export const registerPromptFileContributions = () => { + registerContributions(LANGUAGE_FEATURE_CONTRIBUTIONS); + + registerContribution(ConfigMigration); +}; + +/** + * Type for a generic workbench contribution. + */ +export type TContribution = new (...args: any[]) => IWorkbenchContribution; + +/** + * Register a specific workbench contribution. + */ +const registerContribution = ( + contribution: TContribution, +) => { + Registry.as(Extensions.Workbench) + .registerWorkbenchContribution(contribution, LifecyclePhase.Eventually); +}; + +/** + * Register a specific workbench contribution. + */ +const registerContributions = ( + contributions: readonly TContribution[], +) => { + contributions + .map(registerContribution); +}; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts new file mode 100644 index 00000000000..e296fdecd89 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TContribution } from '../index.js'; +import { PromptLinkProvider } from './providers/promptLinkProvider.js'; +import { isWindows } from '../../../../../../../base/common/platform.js'; +import { PromptPathAutocompletion } from './providers/promptPathAutocompletion.js'; +import { PromptLinkDiagnosticsInstanceManager } from './providers/promptLinkDiagnosticsProvider.js'; +import { PromptHeaderDiagnosticsInstanceManager } from './providers/promptHeaderDiagnosticsProvider.js'; +import { PromptDecorationsProviderInstanceManager } from './providers/decorationsProvider/promptDecorationsProvider.js'; + +/** + * Base list of language feature contributions. + */ +const CONTRIBUTIONS: TContribution[] = [ + PromptLinkProvider, + PromptLinkDiagnosticsInstanceManager, + PromptHeaderDiagnosticsInstanceManager, + PromptDecorationsProviderInstanceManager, +]; + +/** + * We restrict this provider to `Unix` machines for now because of + * the filesystem paths differences on `Windows` operating system. + * + * Notes on `Windows` support: + * - we add the `./` for the first path component, which may not work on `Windows` + * - the first path component of the absolute paths must be a drive letter + */ +if (isWindows === false) { + CONTRIBUTIONS.push(PromptPathAutocompletion); +} + +/** + * List of language feature contributions for the prompt files. + */ +export const LANGUAGE_FEATURE_CONTRIBUTIONS = Object.freeze(CONTRIBUTIONS); diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts new file mode 100644 index 00000000000..b2c0602dff4 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CssClassModifiers } from '../types.js'; +import { localize } from '../../../../../../../../../../nls.js'; +import { FrontMatterMarkerDecoration } from './frontMatterMarkerDecoration.js'; +import { Position } from '../../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../../editor/common/codecs/baseToken.js'; +import { TAddAccessor, TDecorationStyles, ReactiveDecorationBase, asCssVariable } from './utils/index.js'; +import { contrastBorder, editorBackground } from '../../../../../../../../../../platform/theme/common/colorRegistry.js'; +import { ColorIdentifier, darken, registerColor } from '../../../../../../../../../../platform/theme/common/colorUtils.js'; +import { FrontMatterHeader } from '../../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; + +/** + * Decoration CSS class names. + */ +export enum CssClassNames { + main = '.prompt-front-matter-decoration', + inline = '.prompt-front-matter-decoration-inline', + mainInactive = `${CssClassNames.main}${CssClassModifiers.inactive}`, + inlineInactive = `${CssClassNames.inline}${CssClassModifiers.inactive}`, +} + +/** + * Main background color of `active` Front Matter header block. + */ +export const BACKGROUND_COLOR: ColorIdentifier = registerColor( + 'prompt.frontMatter.background', + { dark: darken(editorBackground, 0.2), light: darken(editorBackground, 0.05), hcDark: contrastBorder, hcLight: contrastBorder }, + localize('chat.prompt.frontMatter.background.description', "Background color of a Front Matter header block."), +); + +/** + * Background color of `inactive` Front Matter header block. + */ +export const INACTIVE_BACKGROUND_COLOR: ColorIdentifier = registerColor( + 'prompt.frontMatter.inactiveBackground', + { dark: darken(editorBackground, 0.1), light: darken(editorBackground, 0.025), hcDark: contrastBorder, hcLight: contrastBorder }, + localize('chat.prompt.frontMatter.inactiveBackground.description', "Background color of an inactive Front Matter header block."), +); + +/** + * CSS styles for the decoration. + */ +export const CSS_STYLES = { + [CssClassNames.main]: [ + `background-color: ${asCssVariable(BACKGROUND_COLOR)};`, + 'z-index: -1;', // this is required to allow for selections to appear above the decoration background + ], + [CssClassNames.mainInactive]: [ + `background-color: ${asCssVariable(INACTIVE_BACKGROUND_COLOR)};`, + ], + [CssClassNames.inlineInactive]: [ + 'color: var(--vscode-disabledForeground);', + ], + ...FrontMatterMarkerDecoration.cssStyles, +}; + +/** + * Editor decoration for the Front Matter header token inside a prompt. + */ +export class FrontMatterDecoration extends ReactiveDecorationBase { + constructor( + accessor: TAddAccessor, + token: FrontMatterHeader, + ) { + super(accessor, token); + + this.childDecorators.push( + new FrontMatterMarkerDecoration(accessor, token.startMarker), + new FrontMatterMarkerDecoration(accessor, token.endMarker), + ); + } + + public override setCursorPosition( + position: Position | null | undefined, + ): this is { readonly changed: true } { + const result = super.setCursorPosition(position); + + for (const marker of this.childDecorators) { + if ((marker instanceof FrontMatterMarkerDecoration) === false) { + continue; + } + + // activate/deactivate markers based on the active state + // of the main Front Matter header decoration + marker.activate(this.active); + } + + return result; + } + + protected override get classNames() { + return CssClassNames; + } + + protected override get isWholeLine(): boolean { + return true; + } + + protected override get description(): string { + return 'Front Matter header decoration.'; + } + + public static get cssStyles(): TDecorationStyles { + return CSS_STYLES; + } + + /** + * Whether current decoration class can decorate provided token. + */ + public static handles( + token: BaseToken, + ): token is FrontMatterHeader { + return token instanceof FrontMatterHeader; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts new file mode 100644 index 00000000000..31ad42e50b4 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CssClassModifiers } from '../types.js'; +import { TDecorationStyles, ReactiveDecorationBase } from './utils/index.js'; +import { FrontMatterMarker } from '../../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; + +/** + * Decoration CSS class names. + */ +export enum CssClassNames { + main = '.prompt-front-matter-decoration-marker', + inline = '.prompt-front-matter-decoration-marker-inline', + mainInactive = `${CssClassNames.main}${CssClassModifiers.inactive}`, + inlineInactive = `${CssClassNames.inline}${CssClassModifiers.inactive}`, +} + +/** + * Editor decoration for a `marker` token of a Front Matter header. + */ +export class FrontMatterMarkerDecoration extends ReactiveDecorationBase { + /** + * Activate/deactivate the decoration. + */ + public activate(state: boolean): this { + const position = (state === true) + ? this.token.range.getStartPosition() + : null; + + this.setCursorPosition(position); + + return this; + } + + protected override get classNames() { + return CssClassNames; + } + + protected override get description(): string { + return 'Marker decoration of a Front Matter header.'; + } + + public static get cssStyles(): TDecorationStyles { + return { + [CssClassNames.inline]: [ + 'color: var(--vscode-disabledForeground);', + ], + [CssClassNames.inlineInactive]: [ + 'opacity: 0.25;', + ], + }; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts new file mode 100644 index 00000000000..614a22a7ce9 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../../../../../../../../../editor/common/core/range.js'; +import { IMarkdownString } from '../../../../../../../../../../../base/common/htmlContent.js'; +import { BaseToken } from '../../../../../../../../../../../editor/common/codecs/baseToken.js'; +import { TrackedRangeStickiness } from '../../../../../../../../../../../editor/common/model.js'; +import type { TAddAccessor, TChangeAccessor, TDecorationStyles, TRemoveAccessor } from './types.js'; +import { ModelDecorationOptions } from '../../../../../../../../../../../editor/common/model/textModel.js'; + +/** + * Base class for all editor decorations. + */ +export abstract class DecorationBase< + TPromptToken extends BaseToken, + TCssClassName extends string = string, +> { + /** + * Description of the decoration. + */ + protected abstract get description(): string; + + /** + * Default CSS class name of the decoration. + */ + protected abstract get className(): TCssClassName; + + /** + * Inline CSS class name of the decoration. + */ + protected abstract get inlineClassName(): TCssClassName; + + /** + * Indicates whether the decoration spans the whole line(s). + */ + protected get isWholeLine(): boolean { + return false; + } + + /** + * Hover message of the decoration. + */ + protected get hoverMessage(): IMarkdownString | IMarkdownString[] | null { + return null; + } + + /** + * ID of editor decoration it was registered with. + */ + public readonly id: string; + + constructor( + accessor: TAddAccessor, + protected readonly token: TPromptToken, + ) { + this.id = accessor.addDecoration(this.range, this.decorationOptions); + } + + /** + * Range of the decoration. + */ + public get range(): Range { + return this.token.range; + } + + /** + * Changes the decoration in the editor. + */ + public change( + accessor: TChangeAccessor, + ): this { + accessor.changeDecorationOptions( + this.id, + this.decorationOptions, + ); + + return this; + } + + /** + * Removes associated editor decoration(s). + */ + public remove( + accessor: TRemoveAccessor, + ): this { + accessor.removeDecoration(this.id); + + return this; + } + + /** + * Get editor decoration options for this decorator. + */ + private get decorationOptions(): ModelDecorationOptions { + return ModelDecorationOptions.createDynamic({ + description: this.description, + hoverMessage: this.hoverMessage, + className: this.className, + inlineClassName: this.inlineClassName, + isWholeLine: this.isWholeLine, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + shouldFillLineOnLineBreak: true, + }); + } +} + +/** + * Type of a generic decoration class. + */ +export type TDecorationClass = { + new( + accessor: TAddAccessor, + token: TPromptToken, + ): DecorationBase; + + /** + * CSS styles for the decoration. + */ + readonly cssStyles: TDecorationStyles; + + /** + * Whether the decoration class handles the provided token. + */ + handles(token: BaseToken): token is TPromptToken; +}; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts new file mode 100644 index 00000000000..3980294fc1a --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ColorIdentifier } from '../../../../../../../../../../../platform/theme/common/colorUtils.js'; + +/** + * Convert a registered color to a CSS variable string. + */ +export const asCssVariable = (color: ColorIdentifier): string => { + return `var(--vscode-${color.replaceAll('.', '-')})`; +}; + +export type * from './types.js'; +export { DecorationBase, type TDecorationClass } from './decorationBase.js'; +export { ReactiveDecorationBase, type TChangedDecorator } from './reactiveDecorationBase.js'; + diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts new file mode 100644 index 00000000000..2d07e97574b --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DecorationBase } from './decorationBase.js'; +import { Position } from '../../../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../../../editor/common/codecs/baseToken.js'; +import type { IReactiveDecorationClassNames, TAddAccessor, TChangeAccessor, TRemoveAccessor } from './types.js'; + +/** + * Base class for all reactive editor decorations. A reactive decoration + * is a decoration that can change its appearance based on current cursor + * position in the editor, hence can "react" to the user's actions. + */ +export abstract class ReactiveDecorationBase< + TPromptToken extends BaseToken, + TCssClassName extends string = string, +> extends DecorationBase { + /** + * CSS class names of the decoration. + */ + protected abstract get classNames(): IReactiveDecorationClassNames; + + /** + * A list of child decorators that are part of this decoration. + * For instance a Front Matter header decoration can have child + * decorators for each of the header's `---` markers. + */ + protected readonly childDecorators: DecorationBase[]; + + /** + * Whether the decoration has changed since the last {@link change}. + */ + public get changed(): boolean { + // if any of the child decorators changed, this object is also + // considered to be changed + for (const marker of this.childDecorators) { + if ((marker instanceof ReactiveDecorationBase) === false) { + continue; + } + + if (marker.changed === true) { + return true; + } + } + + return this.didChange; + } + + constructor( + accessor: TAddAccessor, + token: TPromptToken, + ) { + super(accessor, token); + + this.childDecorators = []; + } + + /** + * Current position of cursor in the editor. + */ + private cursorPosition?: Position | null; + + /** + * Private field for the {@link changed} property. + */ + private didChange = true; + + /** + * Whether cursor is currently inside the decoration range. + */ + protected get active(): boolean { + return true; + + /** + * Temporarily disable until we have a proper way to get + * the cursor position inside active editor. + */ + /** + * if (!this.cursorPosition) { + * return false; + * } + * + * // when cursor is at the end of a range, the range considered to + * // not contain the position, but we want to include it + * const atEnd = (this.range.endLineNumber === this.cursorPosition.lineNumber) + * && (this.range.endColumn === this.cursorPosition.column); + * + * return atEnd || this.range.containsPosition(this.cursorPosition); + */ + } + + /** + * Set cursor position and update {@link changed} property if needed. + */ + public setCursorPosition( + position: Position | null | undefined, + ): this is { readonly changed: true } { + if (this.cursorPosition === position) { + return false; + } + + if (this.cursorPosition && position) { + if (this.cursorPosition.equals(position)) { + return false; + } + } + + const wasActive = this.active; + this.cursorPosition = position; + this.didChange = (wasActive !== this.active); + + return this.changed; + } + + public override change( + accessor: TChangeAccessor, + ): this { + if (this.didChange === false) { + return this; + } + + super.change(accessor); + this.didChange = false; + + for (const marker of this.childDecorators) { + marker.change(accessor); + } + + return this; + } + + public override remove( + accessor: TRemoveAccessor, + ): this { + super.remove(accessor); + + for (const marker of this.childDecorators) { + marker.remove(accessor); + } + + return this; + } + + protected override get className() { + return (this.active) + ? this.classNames.main + : this.classNames.mainInactive; + } + + protected override get inlineClassName() { + return (this.active) + ? this.classNames.inline + : this.classNames.inlineInactive; + } +} + +/** + * Type for a decorator with {@link ReactiveDecorationBase.changed changed} property set to `true`. + */ +export type TChangedDecorator = ReactiveDecorationBase & { readonly changed: true }; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts new file mode 100644 index 00000000000..73086f0e860 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from '../../../../../../../../../../../editor/common/model.ts'; + +/** + * CSS class names of a `reactive` decoration. + */ +export interface IReactiveDecorationClassNames { + /** + * Main, default CSS class name of the decoration. + */ + readonly main: T; + + /** + * CSS class name of the decoration for the `inline`(text) styles. + */ + readonly inline: T; + + /** + * main CSS class name of the decoration for the `inactive` + * decoration state. + */ + readonly mainInactive: T; + + /** + * CSS class name of the decoration for the `inline`(text) + * styles when decoration is in the `inactive` state. + */ + readonly inlineInactive: T; +} + +/** + * CSS styles for a decoration to be registered with editor. + */ +export type TDecorationStyles = { + readonly [key in TClassNames]: readonly string[]; +}; + +/** + * A model decorations accessor that can be used to `add` a decoration. + */ +export type TAddAccessor = Pick; + +/** + * A model decorations accessor that can be used to `change` a decoration. + */ +export type TChangeAccessor = Pick; + +/** + * A model decorations accessor that can be used to `remove` a decoration. + */ +export type TRemoveAccessor = Pick; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts new file mode 100644 index 00000000000..0b534a21a63 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../../../../service/types.js'; +import { ProviderInstanceBase } from '../providerInstanceBase.js'; +import { ITextModel } from '../../../../../../../../../editor/common/model.js'; +import { FrontMatterDecoration } from './decorations/frontMatterDecoration.js'; +import { toDisposable } from '../../../../../../../../../base/common/lifecycle.js'; +import { ProviderInstanceManagerBase } from '../providerInstanceManagerBase.js'; +import { Position } from '../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { registerThemingParticipant } from '../../../../../../../../../platform/theme/common/themeService.js'; +import { FrontMatterHeader } from '../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { DecorationBase, ReactiveDecorationBase, type TDecorationClass, type TChangedDecorator } from './decorations/utils/index.js'; + +/** + * Prompt tokens that are decorated by this provider. + */ +type TDecoratedToken = FrontMatterHeader; + +/** + * List of all supported decorations. + */ +const SUPPORTED_DECORATIONS: readonly TDecorationClass[] = Object.freeze([ + FrontMatterDecoration, +]); + +/** + * Prompt syntax decorations provider for text models. + */ +export class PromptDecorator extends ProviderInstanceBase { + /** + * Currently active decorations. + */ + private readonly decorations: DecorationBase[] = []; + + constructor( + model: ITextModel, + @IPromptsService promptsService: IPromptsService, + ) { + super(model, promptsService); + + this.watchCursorPosition(); + } + + protected override async onPromptSettled( + _error?: Error, + ): Promise { + // by the time the promise above completes, either this object + // or the text model might be already has been disposed + if (this.disposed || this.model.isDisposed()) { + return this; + } + + this.addDecorations(); + + return this; + } + + /** + * Get the current cursor position inside an active editor. + * Note! Currently not implemented because the provider is disabled, and + * we need to do some refactoring to get accurate cursor position. + */ + private get cursorPosition(): Position | null { + if (this.model.isDisposed()) { + return null; + } + + return null; + } + + /** + * Watch editor cursor position and update reactive decorations accordingly. + */ + private watchCursorPosition(): this { + const interval = setInterval(() => { + const { cursorPosition } = this; + + const changedDecorations: TChangedDecorator[] = []; + for (const decoration of this.decorations) { + if ((decoration instanceof ReactiveDecorationBase) === false) { + continue; + } + + if (decoration.setCursorPosition(cursorPosition) === true) { + changedDecorations.push(decoration); + } + } + + if (changedDecorations.length === 0) { + return; + } + + this.changeModelDecorations(changedDecorations); + }, 25); + + this._register(toDisposable(() => { + clearInterval(interval); + })); + + return this; + } + + /** + * Update existing decorations. + */ + private changeModelDecorations( + decorations: readonly TChangedDecorator[], + ): this { + this.model.changeDecorations((accessor) => { + for (const decoration of decorations) { + decoration.change(accessor); + } + }); + + return this; + } + + /** + * Add decorations for all prompt tokens. + */ + private addDecorations(): this { + this.model.changeDecorations((accessor) => { + const { tokens } = this.parser; + + // remove all existing decorations + for (const decoration of this.decorations.splice(0)) { + decoration.remove(accessor); + } + + // then add new decorations based on the current tokens + for (const token of tokens) { + for (const Decoration of SUPPORTED_DECORATIONS) { + if (Decoration.handles(token) === false) { + continue; + } + + this.decorations.push( + new Decoration(accessor, token), + ); + break; + } + } + }); + + return this; + } + + /** + * Remove all existing decorations. + */ + private removeAllDecorations(): this { + if (this.decorations.length === 0) { + return this; + } + + this.model.changeDecorations((accessor) => { + for (const decoration of this.decorations.splice(0)) { + decoration.remove(accessor); + } + }); + + return this; + } + + public override dispose(): void { + if (this.disposed) { + return; + } + + this.removeAllDecorations(); + super.dispose(); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `text-model-prompt-decorator:${this.model.uri.path}`; + } +} + +/** + * Register CSS styles of the supported decorations. + */ +registerThemingParticipant((_theme, collector) => { + for (const Decoration of SUPPORTED_DECORATIONS) { + for (const [className, styles] of Object.entries(Decoration.cssStyles)) { + collector.addRule(`.monaco-editor ${className} { ${styles.join(' ')} }`); + } + } +}); + +/** + * Provider for prompt syntax decorators on text models. + */ +export class PromptDecorationsProviderInstanceManager extends ProviderInstanceManagerBase { + protected override get InstanceClass() { + return PromptDecorator; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts similarity index 68% rename from code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts index 85e7f645b75..f1634345088 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/types.d.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRange } from '../../../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; +import { IRange } from '../../../../../../../../../editor/common/core/range.js'; +import { ModelDecorationOptions } from '../../../../../../../../../editor/common/model/textModel.js'; /** * Decoration object. @@ -35,3 +35,14 @@ export enum DecorationClassNames { */ fileReference = DecorationClassNames.default, } + +/** + * Decoration CSS class modifiers. + */ +export enum CssClassModifiers { + /** + * CSS class modifier for `active` state of + * a `reactive` prompt syntax decoration. + */ + inactive = '.prompt-decoration-inactive', +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts new file mode 100644 index 00000000000..a2caf8d250a --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../../../service/types.js'; +import { ProviderInstanceBase } from './providerInstanceBase.js'; +import { assertNever } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js'; +import { TDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../../../parsers/promptHeader/diagnostics.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../../platform/markers/common/markers.js'; + +/** + * Unique ID of the markers provider class. + */ +const MARKERS_OWNER_ID = 'prompts-header-diagnostics-provider'; + +/** + * Prompt header diagnostics provider for an individual text model + * of a prompt file. + */ +class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { + constructor( + model: ITextModel, + @IPromptsService promptsService: IPromptsService, + @IMarkerService private readonly markerService: IMarkerService, + ) { + super(model, promptsService); + } + + /** + * Update diagnostic markers for the current editor. + */ + protected override async onPromptSettled(): Promise { + // ensure that parsing process is settled + await this.parser.allSettled(); + + // clean up all previously added markers + this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); + + const { header } = this.parser; + if (header === undefined) { + return this; + } + + const markers: IMarkerData[] = []; + for (const diagnostic of header.diagnostics) { + markers.push(toMarker(diagnostic)); + } + + this.markerService.changeOne( + MARKERS_OWNER_ID, + this.model.uri, + markers, + ); + + return this; + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `prompt-link-diagnostics:${this.model.uri.path}`; + } +} + +/** + * Convert a provided diagnostic object into a marker data object. + */ +const toMarker = ( + diagnostic: TDiagnostic, +): IMarkerData => { + if (diagnostic instanceof PromptMetadataWarning) { + return { + message: diagnostic.message, + severity: MarkerSeverity.Warning, + ...diagnostic.range, + }; + } + + if (diagnostic instanceof PromptMetadataError) { + return { + message: diagnostic.message, + severity: MarkerSeverity.Error, + ...diagnostic.range, + }; + } + + + assertNever( + diagnostic, + `Unknown prompt metadata diagnostic type '${diagnostic}'.`, + ); +}; + +/** + * The class that manages creation and disposal of {@link PromptHeaderDiagnosticsProvider} + * classes for each specific editor text model. + */ +export class PromptHeaderDiagnosticsInstanceManager extends ProviderInstanceManagerBase { + protected override get InstanceClass() { + return PromptHeaderDiagnosticsProvider; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts new file mode 100644 index 00000000000..222a2c7808f --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../../../service/types.js'; +import { IPromptFileReference } from '../../../parsers/types.js'; +import { ProviderInstanceBase } from './providerInstanceBase.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { NotPromptFile } from '../../../../promptFileReferenceErrors.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../../platform/markers/common/markers.js'; + +/** + * Unique ID of the markers provider class. + */ +const MARKERS_OWNER_ID = 'prompt-link-diagnostics-provider'; + +/** + * Prompt links diagnostics provider for a single text model. + */ +class PromptLinkDiagnosticsProvider extends ProviderInstanceBase { + constructor( + model: ITextModel, + @IPromptsService promptsService: IPromptsService, + @IMarkerService private readonly markerService: IMarkerService, + ) { + super(model, promptsService); + } + + /** + * Update diagnostic markers for the current editor. + */ + protected override async onPromptSettled() { + // ensure that parsing process is settled + await this.parser.allSettled(); + + // clean up all previously added markers + this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); + + const markers: IMarkerData[] = []; + for (const link of this.parser.references) { + const { topError, linkRange } = link; + + if (!topError || !linkRange) { + continue; + } + + const { originalError } = topError; + + // the `NotPromptFile` error is allowed because we allow users + // to include non-prompt file links in the prompt files + // note! this check also handles the `FolderReference` error + if (originalError instanceof NotPromptFile) { + continue; + } + + markers.push(toMarker(link)); + } + + this.markerService.changeOne( + MARKERS_OWNER_ID, + this.model.uri, + markers, + ); + + return this; + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `prompt-link-diagnostics:${this.model.uri.path}`; + } +} + +/** + * Convert a prompt link with an issue to a marker data. + * + * @throws + * - if there is no link issue (e.g., `topError` undefined) + * - if there is no link range to highlight (e.g., `linkRange` undefined) + * - if the original error is of `NotPromptFile` type - we don't want to + * show diagnostic markers for non-prompt file links in the prompts + */ +const toMarker = ( + link: IPromptFileReference, +): IMarkerData => { + const { topError, linkRange } = link; + + // a sanity check because this function must be + // used only if these link attributes are present + assertDefined( + topError, + 'Top error must to be defined.', + ); + assertDefined( + linkRange, + 'Link range must to be defined.', + ); + + const { originalError } = topError; + assert( + !(originalError instanceof NotPromptFile), + 'Error must not be of "not prompt file" type.', + ); + + // `error` severity for the link itself, `warning` for any of its children + const severity = (topError.errorSubject === 'root') + ? MarkerSeverity.Error + : MarkerSeverity.Warning; + + return { + message: topError.localizedMessage, + severity, + ...linkRange, + }; +}; + +/** + * The class that manages creation and disposal of {@link PromptLinkDiagnosticsProvider} + * classes for each specific editor text model. + */ +export class PromptLinkDiagnosticsInstanceManager extends ProviderInstanceManagerBase { + protected override get InstanceClass() { + return PromptLinkDiagnosticsProvider; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts similarity index 61% rename from code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts index 9b40572a7af..8538999ff11 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkProvider.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts @@ -3,20 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LANGUAGE_SELECTOR } from '../constants.js'; -import { IPromptsService } from '../service/types.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { CancellationError } from '../../../../../../base/common/errors.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { Registry } from '../../../../../../platform/registry/common/platform.js'; -import { FolderReference, NotPromptFile } from '../../promptFileReferenceErrors.js'; -import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; -import { ILink, ILinksList, LinkProvider } from '../../../../../../editor/common/languages.js'; -import { IWorkbenchContributionsRegistry, Extensions } from '../../../../../common/contributions.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { IPromptsService } from '../../../service/types.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../../constants.js'; +import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; +import { FolderReference, NotPromptFile } from '../../../../promptFileReferenceErrors.js'; +import { ILink, ILinksList, LinkProvider } from '../../../../../../../../editor/common/languages.js'; +import { ILanguageFeaturesService } from '../../../../../../../../editor/common/services/languageFeatures.js'; /** * Provides link references for prompt files. @@ -28,7 +25,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { ) { super(); - this._register(this.languageService.linkProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.linkProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** @@ -96,7 +93,3 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { }; } } - -// register the provider as a workbench contribution -Registry.as(Extensions.Workbench) - .registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts similarity index 79% rename from code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts index 26f630845d5..a2c8b946b28 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts @@ -14,25 +14,21 @@ * - add `Windows` support */ -import { LANGUAGE_SELECTOR } from '../constants.js'; -import { IPromptsService } from '../service/types.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { assertOneOf } from '../../../../../../base/common/types.js'; -import { isWindows } from '../../../../../../base/common/platform.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { CancellationError } from '../../../../../../base/common/errors.js'; -import { Position } from '../../../../../../editor/common/core/position.js'; -import { IPromptFileReference, IPromptReference } from '../parsers/types.js'; -import { dirname, extUri } from '../../../../../../base/common/resources.js'; -import { assert, assertNever } from '../../../../../../base/common/assert.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { Registry } from '../../../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js'; +import { IPromptsService } from '../../../service/types.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { extUri } from '../../../../../../../../base/common/resources.js'; +import { assertOneOf } from '../../../../../../../../base/common/types.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../../constants.js'; +import { Position } from '../../../../../../../../editor/common/core/position.js'; +import { IPromptFileReference, IPromptReference } from '../../../parsers/types.js'; +import { assert, assertNever } from '../../../../../../../../base/common/assert.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; +import { ILanguageFeaturesService } from '../../../../../../../../editor/common/services/languageFeatures.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../../../editor/common/languages.js'; /** * Type for a filesystem completion item - the one that has its {@link CompletionItem.kind kind} set @@ -100,12 +96,13 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt constructor( @IFileService private readonly fileService: IFileService, - @IPromptsService private readonly promptSyntaxService: IPromptsService, + @IPromptsService private readonly promptsService: IPromptsService, @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + ) { super(); - this._register(this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.completionProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** @@ -136,7 +133,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt `Prompt path autocompletion provider`, ); - const parser = this.promptSyntaxService.getSyntaxParserFor(model); + const parser = this.promptsService.getSyntaxParserFor(model); assert( !parser.disposed, 'Prompt parser must not be disposed.', @@ -159,7 +156,13 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt return undefined; } - const modelDirname = dirname(model.uri); + const { parentFolder } = parser; + + // if didn't find a folder URI to start the suggestions from, + // don't provide any suggestions + if (parentFolder === null) { + return undefined; + } // in the case of the '.' trigger character, we must check if this is the first // dot in the link path, otherwise the dot could be a part of a folder name @@ -167,7 +170,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt return { suggestions: await this.getFirstFolderSuggestions( triggerCharacter, - modelDirname, + parentFolder, fileReference, ), }; @@ -177,7 +180,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt return { suggestions: await this.getNonFirstFolderSuggestions( triggerCharacter, - modelDirname, + parentFolder, fileReference, ), }; @@ -307,8 +310,8 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt return []; } - const currenFolder = extUri.resolvePath(fileFolderUri, path); - let suggestions = await this.getFolderSuggestions(currenFolder); + const currentFolder = extUri.resolvePath(fileFolderUri, path); + let suggestions = await this.getFolderSuggestions(currentFolder); // when trigger character was a `.`, which is we know is inside // the folder/file name in the path, filter out to only items @@ -344,17 +347,3 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt }); } } - -/** - * We restrict this provider to `Unix` machines for now because of - * the filesystem paths differences on `Windows` operating system. - * - * Notes on `Windows` support: - * - we add the `./` for the first path component, which may not work on `Windows` - * - the first path component of the absolute paths must be a drive letter - */ -if (!isWindows) { - // register the provider as a workbench contribution - Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(PromptPathAutocompletion, LifecyclePhase.Eventually); -} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts new file mode 100644 index 00000000000..f42d4763485 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService, TSharedPrompt } from '../../../service/types.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { ObservableDisposable } from '../../../../../../../../base/common/observableDisposable.js'; + +/** + * Abstract base class for all reusable prompt file providers. + */ +export abstract class ProviderInstanceBase extends ObservableDisposable { + /** + * Function that is called when the prompt parser is settled. + */ + protected abstract onPromptSettled(error: Error | undefined): Promise; + + /** + * Returns a string representation of this object. + */ + public abstract override toString(): string; + + /** + * The prompt parser instance. + */ + protected readonly parser: TSharedPrompt; + + constructor( + protected readonly model: ITextModel, + @IPromptsService promptsService: IPromptsService, + ) { + super(); + + this.parser = promptsService.getSyntaxParserFor(model); + + this._register( + this.parser.onSettled(this.onPromptSettled.bind(this)), + ); + this.parser + .onDispose(this.dispose.bind(this)) + .start(); + + // initialize an update + setTimeout(this.onPromptSettled.bind(this)); + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts new file mode 100644 index 00000000000..c766123e204 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProviderInstanceBase } from './providerInstanceBase.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { ObjectCache } from '../../../../../../../../base/common/objectCache.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../constants.js'; +import { IModelService } from '../../../../../../../../editor/common/services/model.js'; +import { PromptsConfig } from '../../../../../../../../platform/prompts/common/config.js'; +import { IEditorService } from '../../../../../../../services/editor/common/editorService.js'; +import { IDiffEditor, IEditor, IEditorModel } from '../../../../../../../../editor/common/editorCommon.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../../../../platform/configuration/common/configuration.js'; + +/** + * Type for a text editor that is used for reusable prompt files. + */ +export interface IPromptFileEditor extends IEditor { + readonly getModel: () => ITextModel; +} + +/** + * A generic base class that manages creation and disposal of {@link TInstance} + * objects for each specific editor object that is used for reusable prompt files. + */ +export abstract class ProviderInstanceManagerBase extends Disposable { + /** + * Currently available {@link TInstance} instances. + */ + private readonly instances: ObjectCache; + + /** + * Class object of the managed {@link TInstance}. + */ + protected abstract get InstanceClass(): new (editor: ITextModel, ...args: any[]) => TInstance; + + constructor( + @IModelService modelService: IModelService, + @IEditorService editorService: IEditorService, + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + ) { + super(); + + // cache of managed instances + this.instances = this._register( + new ObjectCache((model: ITextModel) => { + assert( + model.isDisposed() === false, + 'Text model must not be disposed.', + ); + + // sanity check - the new TS/JS discrepancies regarding fields initialization + // logic mean that this can be `undefined` during runtime while defined in TS + assertDefined( + this.InstanceClass, + 'Instance class field must be defined.', + ); + + const instance: TInstance = initService.createInstance( + this.InstanceClass, + model, + ); + + // this is a sanity check and the contract of the object cache, + // we must return a non-disposed object from this factory function + instance.assertNotDisposed( + 'Created instance must not be disposed.', + ); + + return instance; + }), + ); + + // if the feature is disabled, do not create any providers + if (PromptsConfig.enabled(configService) === false) { + return; + } + + // subscribe to changes of the active editor + this._register(editorService.onDidActiveEditorChange(() => { + const { activeTextEditorControl } = editorService; + if (activeTextEditorControl === undefined) { + return; + } + + this.handleNewEditor(activeTextEditorControl); + })); + + // handle existing visible text editors + editorService + .visibleTextEditorControls + .forEach(this.handleNewEditor.bind(this)); + + // subscribe to "language change" events for all models + this._register( + modelService.onModelLanguageChanged((event) => { + const { model, oldLanguageId } = event; + + // if language is set to `prompt` or `instructions` language, handle that model + if (isPromptFileModel(model)) { + this.instances.get(model); + return; + } + + // if the language is changed away from `prompt` or `instructions`, + // remove and dispose provider for this model + if (isPromptOrInstructionsFile(oldLanguageId)) { + this.instances.remove(model, true); + return; + } + }), + ); + } + + /** + * Initialize a new {@link TInstance} for the given editor. + */ + private handleNewEditor(editor: IEditor | IDiffEditor): this { + const model = editor.getModel(); + if (model === null) { + return this; + } + + if (isPromptFileModel(model) === false) { + return this; + } + + // note! calling `get` also creates a provider if it does not exist; + // and the provider is auto-removed when the editor is disposed + this.instances.get(model); + + return this; + } +} + +/** + * Check if provided language ID is either + * the `prompt` or `instructions` one. + */ +const isPromptOrInstructionsFile = ( + languageId: string, +): boolean => { + return (languageId === PROMPT_LANGUAGE_ID) || (languageId === INSTRUCTIONS_LANGUAGE_ID); +}; + +/** + * Check if a provided model is used for prompt files. + */ +const isPromptFileModel = ( + model: IEditorModel, +): model is ITextModel => { + // we support only `text editors` for now so filter out `diff` ones + if ('modified' in model || 'model' in model) { + return false; + } + + if (model.isDisposed()) { + return false; + } + + if (isPromptOrInstructionsFile(model.getLanguageId()) === false) { + return false; + } + + return true; +}; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts deleted file mode 100644 index 7d46c72dbbe..00000000000 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts +++ /dev/null @@ -1,224 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPromptsService } from '../service/types.js'; -import { IPromptFileReference } from '../parsers/types.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { NotPromptFile } from '../../promptFileReferenceErrors.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IEditor } from '../../../../../../editor/common/editorCommon.js'; -import { ObjectCache } from '../../../../../../base/common/objectCache.js'; -import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; -import { Registry } from '../../../../../../platform/registry/common/platform.js'; -import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; -import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; -import { IWorkbenchContributionsRegistry, Extensions } from '../../../../../common/contributions.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; - -/** - * Unique ID of the markers provider class. - */ -const MARKERS_OWNER_ID = 'reusable-prompts-syntax'; - -/** - * Prompt links diagnostics provider for a single text model. - */ -class PromptLinkDiagnosticsProvider extends ObservableDisposable { - /** - * Reference to the current prompt syntax parser instance. - */ - private readonly parser: TextModelPromptParser; - - constructor( - private readonly editor: ITextModel, - @IMarkerService private readonly markerService: IMarkerService, - @IPromptsService private readonly promptsService: IPromptsService, - ) { - super(); - - this.parser = this.promptsService - .getSyntaxParserFor(this.editor) - .onUpdate(this.updateMarkers.bind(this)) - .onDispose(this.dispose.bind(this)) - .start(); - - // initialize markers - this.updateMarkers(); - } - - /** - * Update diagnostic markers for the current editor. - */ - private async updateMarkers() { - // ensure that parsing process is settled - await this.parser.allSettled(); - - // clean up all previously added markers - this.markerService.remove(MARKERS_OWNER_ID, [this.editor.uri]); - - const markers: IMarkerData[] = []; - for (const link of this.parser.references) { - const { topError, linkRange } = link; - - if (!topError || !linkRange) { - continue; - } - - const { originalError } = topError; - - // the `NotPromptFile` error is allowed because we allow users - // to include non-prompt file links in the prompt files - // note! this check also handles the `FolderReference` error - if (originalError instanceof NotPromptFile) { - continue; - } - - markers.push(toMarker(link)); - } - - this.markerService.changeOne( - MARKERS_OWNER_ID, - this.editor.uri, - markers, - ); - } -} - -/** - * Convert a prompt link with an issue to a marker data. - * - * @throws - * - if there is no link issue (e.g., `topError` undefined) - * - if there is no link range to highlight (e.g., `linkRange` undefined) - * - if the original error is of `NotPromptFile` type - we don't want to - * show diagnostic markers for non-prompt file links in the prompts - */ -const toMarker = ( - link: IPromptFileReference, -): IMarkerData => { - const { topError, linkRange } = link; - - // a sanity check because this function must be - // used only if these link attributes are present - assertDefined( - topError, - 'Top error must to be defined.', - ); - assertDefined( - linkRange, - 'Link range must to be defined.', - ); - - const { originalError } = topError; - assert( - !(originalError instanceof NotPromptFile), - 'Error must not be of "not prompt file" type.', - ); - - // `error` severity for the link itself, `warning` for any of its children - const severity = (topError.errorSubject === 'root') - ? MarkerSeverity.Error - : MarkerSeverity.Warning; - - return { - message: topError.localizedMessage, - severity, - ...linkRange, - }; -}; - -/** - * The class that manages creation and disposal of {@link PromptLinkDiagnosticsProvider} - * classes for each specific editor text model. - */ -export class PromptLinkDiagnosticsInstanceManager extends Disposable { - /** - * Currently available {@link PromptLinkDiagnosticsProvider} instances. - */ - private readonly providers: ObjectCache; - - constructor( - @IEditorService editorService: IEditorService, - @IInstantiationService initService: IInstantiationService, - @IConfigurationService configService: IConfigurationService, - ) { - super(); - - // cache of prompt marker providers - this.providers = this._register( - new ObjectCache((editor: ITextModel) => { - const parser: PromptLinkDiagnosticsProvider = initService.createInstance( - PromptLinkDiagnosticsProvider, - editor, - ); - - // this is a sanity check and the contract of the object cache, - // we must return a non-disposed object from this factory function - parser.assertNotDisposed( - 'Created prompt parser must not be disposed.', - ); - - return parser; - }), - ); - - // if the feature is disabled, do not create any providers - if (!PromptsConfig.enabled(configService)) { - return; - } - - // subscribe to changes of the active editor - this._register(editorService.onDidActiveEditorChange(() => { - const { activeTextEditorControl } = editorService; - if (!activeTextEditorControl) { - return; - } - - this.handleNewEditor(activeTextEditorControl); - })); - - // handle existing visible text editors - editorService - .visibleTextEditorControls - .forEach(this.handleNewEditor.bind(this)); - } - - /** - * Initialize a new {@link PromptLinkDiagnosticsProvider} for the given editor. - */ - private handleNewEditor(editor: IEditor): this { - const model = editor.getModel(); - if (!model) { - return this; - } - - // we support only `text editors` for now so filter out `diff` ones - if ('modified' in model || 'model' in model) { - return this; - } - - // enable this only for prompt file editors - if (!isPromptFile(model.uri)) { - return this; - } - - // note! calling `get` also creates a provider if it does not exist; - // and the provider is auto-removed when the model is disposed - this.providers.get(model); - - return this; - } -} - -// register the provider as a workbench contribution -Registry.as(Extensions.Workbench) - .registerWorkbenchContribution(PromptLinkDiagnosticsInstanceManager, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index bd5dc68c831..31c6bb08494 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -4,26 +4,56 @@ *--------------------------------------------------------------------------------------------*/ import { TopError } from './topError.js'; +import { ChatMode } from '../../constants.js'; +import { PromptHeader } from './promptHeader/header.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { PromptToken } from '../codecs/tokens/promptToken.js'; +import * as path from '../../../../../../base/common/path.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { FileReference } from '../codecs/tokens/fileReference.js'; import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; -import { IRange } from '../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../base/common/types.js'; import { IPromptContentsProvider } from '../contentProviders/types.js'; -import { IPromptReference, IResolveError, ITopError } from './types.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { DeferredPromise } from '../../../../../../base/common/async.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { PromptVariableWithData } from '../codecs/tokens/promptVariable.js'; -import { basename, extUri } from '../../../../../../base/common/resources.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; import { assert, assertNever } from '../../../../../../base/common/assert.js'; +import { basename, dirname } from '../../../../../../base/common/resources.js'; +import { BaseToken } from '../../../../../../editor/common/codecs/baseToken.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; +import { IPromptMetadata, IPromptReference, IResolveError, ITopError } from './types.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { isPromptOrInstructionsFile } from '../../../../../../platform/prompts/common/constants.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { MarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownToken.js'; +import { FrontMatterHeader } from '../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; +import { IPromptContentsProviderOptions, DEFAULT_OPTIONS as CONTENTS_PROVIDER_DEFAULT_OPTIONS } from '../contentProviders/promptContentsProviderBase.js'; + +/** + * Options of the {@link BasePromptParser} class. + */ +export interface IPromptParserOptions extends IPromptContentsProviderOptions { + /** + * List of reference paths have been already seen before + * getting to the current prompt. Used to prevent infinite + * recursion in prompt file references. + */ + readonly seenReferences: readonly string[]; +} + +/** + * Default {@link IPromptContentsProviderOptions} options. + */ +const DEFAULT_OPTIONS: IPromptParserOptions = { + ...CONTENTS_PROVIDER_DEFAULT_OPTIONS, + seenReferences: [], +}; /** * Error conditions that may happen during the file reference resolution. @@ -35,16 +65,72 @@ export type TErrorCondition = OpenFailed | RecursiveReference | FolderReference * prompt parsers that are responsible for parsing chat prompt syntax. */ export class BasePromptParser extends ObservableDisposable { + /** + * Options passed to the constructor, extended with + * value defaults from {@link DEFAULT_OPTIONS}. + */ + protected readonly options: IPromptParserOptions; + + /** + * List of all tokens that were parsed from the prompt contents so far. + */ + public get tokens(): readonly BaseToken[] { + return [...this.receivedTokens]; + } + /** + * Private field behind the readonly {@link tokens} property. + */ + private receivedTokens: BaseToken[] = []; + /** * List of file references in the current branch of the file reference tree. */ private readonly _references: IPromptReference[] = []; + /** + * Reference to the prompt header object that holds metadata associated + * with the prompt. + */ + private promptHeader?: PromptHeader; + + /** + * Reference to the prompt header object that holds metadata associated + * with the prompt. + */ + public get header(): PromptHeader | undefined { + return this.promptHeader; + } + /** * The event is fired when lines or their content change. */ private readonly _onUpdate = this._register(new Emitter()); + /** + * Event that is fired when the current prompt parser is settled. + */ + private readonly _onSettled = this._register(new Emitter()); + + /** + * Event that is fired when the current prompt parser is settled. + */ + public onSettled( + callback: (error?: Error) => void, + ): IDisposable { + const disposable = this._onSettled.event(callback); + const streamEnded = (this.stream?.ended && (this.stream.disposed === false)); + + // if already in the error state or stream has already ended, + // invoke the callback immediately but asynchronously + if (streamEnded || this.errorCondition) { + setTimeout(callback.bind(undefined, this.errorCondition)); + + return disposable; + } + + return disposable; + } + /** * Subscribe to the `onUpdate` event that is fired when prompt tokens are updated. * @param callback The callback function to be called on updates. @@ -108,6 +194,12 @@ export class BasePromptParser return this; } + // by the time when the `firstParseResult` promise is resolved, + // this object may have been already disposed, hence noop + if (this.disposed) { + return this; + } + assertDefined( this.stream, 'No stream reference found.', @@ -115,11 +207,16 @@ export class BasePromptParser await this.stream.settled; + // if prompt header exists, also wait for it to be settled + if (this.promptHeader) { + await this.promptHeader.settled; + } + return this; } /** - * Same as {@linkcode settled} but also waits for all possible + * Same as {@link settled} but also waits for all possible * nested child prompt references and their children to be settled. */ public async allSettled(): Promise { @@ -136,14 +233,22 @@ export class BasePromptParser constructor( private readonly promptContentsProvider: TContentsProvider, - seenReferences: string[] = [], + options: Partial, @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @ILogService protected readonly logService: ILogService, ) { super(); + this.options = { + ...DEFAULT_OPTIONS, + ...options, + }; + this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); + const seenReferences = [...this.options.seenReferences]; + // to prevent infinite file recursion, we keep track of all references in // the current branch of the file reference tree and check if the current // file reference has been already seen before @@ -203,6 +308,11 @@ export class BasePromptParser this.stream?.dispose(); delete this.stream; delete this._errorCondition; + this.receivedTokens = []; + + // cleanup current prompt header object + this.promptHeader?.dispose(); + delete this.promptHeader; // dispose all currently existing references this.disposeReferences(); @@ -212,6 +322,9 @@ export class BasePromptParser this._errorCondition = streamOrError; this._onUpdate.fire(); + // when error received fire the 'onSettled' event immediately + this._onSettled.fire(streamOrError); + return; } @@ -224,6 +337,22 @@ export class BasePromptParser // when some tokens received, process and store the references this.stream.on('data', (token) => { + // store all markdown and prompt token references + if ((token instanceof MarkdownToken) || (token instanceof PromptToken)) { + this.receivedTokens.push(token); + } + + // if a prompt header token received, create a new prompt header instance + if (token instanceof FrontMatterHeader) { + this.promptHeader = new PromptHeader( + token.contentToken, + this.promptContentsProvider.languageId, + ).start(); + + return; + } + + // try to convert a prompt variable with data token into a file reference if (token instanceof PromptVariableWithData) { try { this.onReference(FileReference.from(token), [...seenReferences]); @@ -259,12 +388,16 @@ export class BasePromptParser token: FileReference | MarkdownLink, seenReferences: string[], ): this { + const { parentFolder } = this; + + const referenceUri = ((parentFolder !== null) && (path.isAbsolute(token.path) === false)) + ? URI.joinPath(parentFolder, token.path) + : URI.file(token.path); - const referenceUri = extUri.resolvePath(this.dirname, token.path); const contentProvider = this.promptContentsProvider.createNew({ uri: referenceUri }); const reference = this.instantiationService - .createInstance(PromptReference, contentProvider, token, seenReferences); + .createInstance(PromptReference, contentProvider, token, { seenReferences }); // the content provider is exclusively owned by the reference // hence dispose it when the reference is disposed @@ -287,9 +420,16 @@ export class BasePromptParser * @param error Optional error object if stream ended with an error. */ private onStreamEnd( - _stream: ChatPromptDecoder, + stream: ChatPromptDecoder, error?: Error, ): this { + // decoders can fire the 'end' event also when they are get disposed, + // but because we dispose them when a new stream is received, we can + // safely ignore the event in this case + if (stream.disposed === true) { + return this; + } + if (error) { this.logService.warn( `[prompt parser][${basename(this.uri)}] received an error on the chat prompt decoder stream: ${error}`, @@ -297,6 +437,7 @@ export class BasePromptParser } this._onUpdate.fire(); + this._onSettled.fire(error); return this; } @@ -313,7 +454,7 @@ export class BasePromptParser } /** - * Private attribute to track if the {@linkcode start} + * Private attribute to track if the {@link start} * method has been already called at least once. */ private started: boolean = false; @@ -347,10 +488,26 @@ export class BasePromptParser } /** - * Get the parent folder of the file reference. + * Get the parent folder URI of the prompt. + * For instance, if prompt URI points to a file on a disk, this + * function will return the folder URI that contains that file, + * but if the URI points to an `untitled` document, will try to + * use a different folder URI based on the workspace state. */ - public get dirname() { - return URI.joinPath(this.uri, '..'); + public get parentFolder(): URI | null { + if (this.uri.scheme === 'file') { + return dirname(this.uri); + } + + const { folders } = this.workspaceService.getWorkspace(); + + // single-root workspace, use root folder URI + if (folders.length === 1) { + return folders[0].uri; + } + + // if a multi-root workspace, or no workspace at all + return null; } /** @@ -410,6 +567,79 @@ export class BasePromptParser .map(child => child.uri); } + /** + * Valid metadata records defined in the prompt header. + */ + public get metadata(): IPromptMetadata { + if (this.header === undefined) { + return {}; + } + + const { metadata } = this.header; + if (metadata === undefined) { + return {}; + } + + const { tools, mode, description, applyTo } = metadata; + + // compute resulting mode based on presence + // of `tools` metadata in the prompt header + const resultingMode = (tools !== undefined) + ? ChatMode.Agent + : mode?.chatMode; + + return { + mode: resultingMode, + description: description?.text, + tools: tools?.toolNames, + applyTo: applyTo?.text, + }; + } + + /** + * Entire associated `tools` metadata for this reference and + * all possible nested child references. + */ + public get allToolsMetadata(): readonly string[] | null { + let hasTools = false; + const result: string[] = []; + + const { tools, mode } = this.metadata; + + if (tools !== undefined) { + result.push(...tools); + hasTools = true; + } + + const isRootInAgentMode = ((hasTools === true) || (mode === ChatMode.Agent)); + + // the top-level mode defines the overall mode for all + // nested prompt references, therefore if mode of + // the top-level prompt is not equal to `agent`, then + // ignore all `tools` metadata of the nested references + if (isRootInAgentMode === false) { + return null; + } + + for (const reference of this.references) { + const { allToolsMetadata } = reference; + + if (allToolsMetadata === null) { + continue; + } + + result.push(...allToolsMetadata); + hasTools = true; + } + + if (hasTools === false) { + return null; + } + + // return unique list of tools + return [...new Set(result)]; + } + /** * Get list of errors for the direct links of the current reference. */ @@ -510,7 +740,7 @@ export class BasePromptParser * Check if the current reference points to a prompt snippet file. */ public get isPromptFile(): boolean { - return isPromptFile(this.uri); + return isPromptOrInstructionsFile(this.uri); } /** @@ -529,8 +759,12 @@ export class BasePromptParser } this.disposeReferences(); + this.stream?.dispose(); - this._onUpdate.fire(); + delete this.stream; + + this.promptHeader?.dispose(); + delete this.promptHeader; super.dispose(); } @@ -542,10 +776,6 @@ export class BasePromptParser * a markdown link(`[#file:file.md](/path/to/file.md)`). */ export class PromptReference extends ObservableDisposable implements IPromptReference { - public readonly range = this.token.range; - public readonly path: string = this.token.path; - public readonly text: string = this.token.text; - /** * Instance of underlying prompt parser object. */ @@ -554,7 +784,7 @@ export class PromptReference extends ObservableDisposable implements IPromptRefe constructor( private readonly promptContentsProvider: IPromptContentsProvider, public readonly token: FileReference | MarkdownLink, - seenReferences: string[] = [], + options: Partial = {}, @IInstantiationService initService: IInstantiationService, ) { super(); @@ -562,7 +792,7 @@ export class PromptReference extends ObservableDisposable implements IPromptRefe this.parser = this._register(initService.createInstance( BasePromptParser, this.promptContentsProvider, - seenReferences, + options, )); } @@ -640,6 +870,18 @@ export class PromptReference extends ObservableDisposable implements IPromptRefe return this; } + public get range(): Range { + return this.token.range; + } + + public get path(): string { + return this.token.path; + } + + public get text(): string { + return this.token.text; + } + public get resolveFailed(): boolean | undefined { return this.parser.resolveFailed; } @@ -676,6 +918,14 @@ export class PromptReference extends ObservableDisposable implements IPromptRefe return this.parser.allReferences; } + public get metadata(): IPromptMetadata { + return this.parser.metadata; + } + + public get allToolsMetadata(): readonly string[] | null { + return this.parser.allToolsMetadata; + } + public get allValidReferences(): readonly IPromptReference[] { return this.parser.allValidReferences; } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts index 33a2edd93b3..ab4cd1fb7bc 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePromptParser } from './basePromptParser.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** @@ -16,12 +17,13 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ export class FilePromptParser extends BasePromptParser { constructor( uri: URI, - seenReferences: string[] = [], + options: Partial = {}, @IInstantiationService initService: IInstantiationService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { - const contentsProvider = initService.createInstance(FilePromptContentProvider, uri); - super(contentsProvider, seenReferences, initService, logService); + const contentsProvider = initService.createInstance(FilePromptContentProvider, uri, options); + super(contentsProvider, options, initService, workspaceService, logService); this._register(contentsProvider); } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts new file mode 100644 index 00000000000..754293f6dcf --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../../../../../editor/common/core/range.js'; + +/** + * List of all currently supported diagnostic types. + */ +export type TDiagnostic = PromptMetadataWarning | PromptMetadataError; + +/** + * Diagnostics object that hold information about some issue + * related to the prompt header metadata. + */ +export abstract class PromptMetadataDiagnostic { + constructor( + public readonly range: Range, + public readonly message: string, + ) { } + + /** + * String representation of the diagnostic object. + */ + public abstract toString(): string; +} + +/** + * Diagnostics object that hold information about some + * non-fatal issue related to the prompt header metadata. + */ +export class PromptMetadataWarning extends PromptMetadataDiagnostic { + public override toString(): string { + return `warning(${this.message})${this.range}`; + } +} + +/** + * Diagnostics object that hold information about some + * fatal issue related to the prompt header metadata. + */ +export class PromptMetadataError extends PromptMetadataDiagnostic { + public override toString(): string { + return `error(${this.message})${this.range}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts new file mode 100644 index 00000000000..0a5cf917f08 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -0,0 +1,310 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChatMode } from '../../../constants.js'; +import { localize } from '../../../../../../../nls.js'; +import { PromptApplyToMetadata } from './metadata/applyTo.js'; +import { assert } from '../../../../../../../base/common/assert.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { Text } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { PromptMetadataError, PromptMetadataWarning, TDiagnostic } from './diagnostics.js'; +import { TokenStream } from '../../../../../../../editor/common/codecs/utils/tokenStream.js'; +import { SimpleToken } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; +import { PromptToolsMetadata, PromptModeMetadata, PromptDescriptionMetadata } from './metadata/index.js'; +import { FrontMatterRecord } from '../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; +import { FrontMatterDecoder, TFrontMatterToken } from '../../../../../../../editor/common/codecs/frontMatterCodec/frontMatterDecoder.js'; + +/** + * Metadata defined in the prompt header. + */ +export interface IHeaderMetadata { + /** + * Tools metadata in the prompt header. + */ + tools?: PromptToolsMetadata; + + /** + * Description metadata in the prompt header. + */ + description?: PromptDescriptionMetadata; + + /** + * Chat mode metadata in the prompt header. + */ + mode?: PromptModeMetadata; + + /** + * Chat 'applyTo' metadata in the prompt header. + */ + applyTo?: PromptApplyToMetadata; +} + +/** + * Prompt header holds all metadata records for a prompt. + */ +export class PromptHeader extends Disposable { + /** + * Underlying decoder for a Front Matter header. + */ + private readonly stream: FrontMatterDecoder; + + /** + * Metadata records. + */ + private readonly meta: IHeaderMetadata; + /** + * Metadata records. + */ + public get metadata(): Readonly { + return Object.freeze({ + ...this.meta, + }); + } + + /** + * List of all unique metadata record names. + */ + private readonly recordNames: Set; + + /** + * List of all issues found while parsing the prompt header. + */ + private readonly issues: TDiagnostic[]; + + /** + * List of all diagnostic issues found while parsing + * the prompt header. + */ + public get diagnostics(): readonly TDiagnostic[] { + return this.issues; + } + + constructor( + public readonly contentsToken: Text, + public readonly languageId: string, + ) { + super(); + + this.issues = []; + this.meta = {}; + this.recordNames = new Set(); + + this.stream = this._register( + new FrontMatterDecoder( + new TokenStream(contentsToken.tokens), + ), + ); + this.stream.onData(this.onData.bind(this)); + this.stream.onError(this.onError.bind(this)); + } + + /** + * Process front matter tokens, converting them into + * well-known prompt metadata records. + */ + private onData(token: TFrontMatterToken): void { + // we currently expect only front matter 'records' for + // the prompt metadata, hence add diagnostics for all + // other tokens and ignore them + if ((token instanceof FrontMatterRecord) === false) { + // unless its a simple token, in which case we just ignore it + if (token instanceof SimpleToken) { + return; + } + + this.issues.push( + new PromptMetadataError( + token.range, + localize( + 'prompt.header.diagnostics.unexpected-token', + "Unexpected token '{0}'.", + token.text, + ), + ), + ); + + return; + } + + const recordName = token.nameToken.text; + + // if we already have a record with this name, + // add a warning diagnostic and ignore it + if (this.recordNames.has(recordName)) { + this.issues.push( + new PromptMetadataWarning( + token.range, + localize( + 'prompt.header.metadata.diagnostics.duplicate-record', + "Duplicate metadata record '{0}' will be ignored.", + recordName, + ), + ), + ); + + return; + } + + // if the record might be a "description" metadata + // add it to the list of parsed metadata records + if (PromptDescriptionMetadata.isDescriptionRecord(token)) { + const descriptionMetadata = new PromptDescriptionMetadata(token, this.languageId); + const { diagnostics } = descriptionMetadata; + + this.issues.push(...diagnostics); + this.meta.description = descriptionMetadata; + this.recordNames.add(recordName); + return; + } + + // if the record might be a "tools" metadata + // add it to the list of parsed metadata records + if (PromptToolsMetadata.isToolsRecord(token)) { + const toolsMetadata = new PromptToolsMetadata(token, this.languageId); + const { diagnostics } = toolsMetadata; + + this.issues.push(...diagnostics); + this.meta.tools = toolsMetadata; + this.recordNames.add(recordName); + + return this.validateToolsAndModeCompatibility(); + } + + // if the record might be a "mode" metadata + // add it to the list of parsed metadata records + if (PromptModeMetadata.isModeRecord(token)) { + const modeMetadata = new PromptModeMetadata(token, this.languageId); + const { diagnostics } = modeMetadata; + + this.issues.push(...diagnostics); + this.meta.mode = modeMetadata; + this.recordNames.add(recordName); + + return this.validateToolsAndModeCompatibility(); + } + + // if the record might be a "applyTo" metadata + // add it to the list of parsed metadata records + if (PromptApplyToMetadata.isApplyToRecord(token)) { + const applyToMetadata = new PromptApplyToMetadata(token, this.languageId); + const { diagnostics } = applyToMetadata; + + this.issues.push(...diagnostics); + this.meta.applyTo = applyToMetadata; + this.recordNames.add(recordName); + + return; + } + + // all other records are currently not supported + this.issues.push( + new PromptMetadataWarning( + token.range, + localize( + 'prompt.header.metadata.diagnostics.unknown-record', + "Unknown metadata record '{0}' will be ignored.", + recordName, + ), + ), + ); + } + + /** + * Check if value of `tools` and `mode` metadata + * are compatible with each other. + */ + private get toolsAndModeCompatible(): boolean { + const { tools, mode } = this.meta; + + // if `tools` is not set, then the mode metadata + // can have any value so skip the validation + if (tools === undefined) { + return true; + } + + // if `mode` is not set or equal to `agent` mode, + // then the tools metadata can have any value so noop + if ((mode === undefined) || (mode.chatMode === ChatMode.Agent)) { + return true; + } + + // in the other cases when `tools` are defined and `mode` is not + // equal to `agent`, then the `tools` and `mode` are incompatible + return false; + } + + /** + * Validate that the `tools` and `mode` metadata are compatible + * with each other. If not, add a warning diagnostic. + */ + private validateToolsAndModeCompatibility(): void { + if (this.toolsAndModeCompatible === true) { + return; + } + + const { tools, mode } = this.meta; + + // sanity checks on the behavior of the `toolsAndModeCompatible` getter + assertDefined( + tools, + 'Tools metadata must have been present.', + ); + assertDefined( + mode, + 'Mode metadata must have been present.', + ); + assert( + mode.chatMode !== ChatMode.Agent, + 'Mode metadata must not be agent mode.', + ); + + this.issues.push( + new PromptMetadataWarning( + mode.range, + localize( + 'prompt.header.metadata.mode.diagnostics.incompatible-with-tools', + "Record '{0}' is implied to have the '{1}' value if '{2}' record is present so the specified value will be ignored.", + mode.recordName, + ChatMode.Agent, + tools.recordName, + ), + ), + ); + } + + /** + * Process errors from the underlying front matter decoder. + */ + private onError(error: Error): void { + this.issues.push( + new PromptMetadataError( + this.contentsToken.range, + localize( + 'prompt.header.diagnostics.parsing-error', + "Failed to parse prompt header: {0}", + error.message, + ), + ), + ); + } + + /** + * Promise that resolves when parsing process of + * the prompt header completes. + */ + public get settled(): Promise { + return this.stream.settled; + } + + /** + * Starts the parsing process of the prompt header. + */ + public start(): this { + this.stream.start(); + + return this; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts new file mode 100644 index 00000000000..62fd0dae572 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptStringMetadata } from './record.js'; +import { localize } from '../../../../../../../../nls.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../constants.js'; +import { isEmptyPattern, parse } from '../../../../../../../../base/common/glob.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; +import { FrontMatterRecord, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the metadata record in the prompt header. + */ +const RECORD_NAME = 'applyTo'; + +/** + * Prompt `applyTo` metadata record inside the prompt header. + */ +export class PromptApplyToMetadata extends PromptStringMetadata { + constructor( + recordToken: FrontMatterRecord, + languageId: string, + ) { + super(RECORD_NAME, recordToken, languageId); + } + + public override get recordName(): string { + return RECORD_NAME; + } + + protected override validate(): readonly PromptMetadataDiagnostic[] { + const result: PromptMetadataDiagnostic[] = [ + ...super.validate(), + ]; + + // if we don't have a value token, validation must + // has failed already so nothing to do more + if (this.valueToken === undefined) { + return result; + } + + // the applyTo metadata makes sense only for 'instruction' prompts + if (this.languageId !== INSTRUCTIONS_LANGUAGE_ID) { + result.push( + new PromptMetadataError( + this.range, + localize( + 'prompt.header.metadata.string.diagnostics.invalid-language', + "The '{0}' metadata record is only valid in instruction files.", + this.recordName, + ), + ), + ); + + delete this.valueToken; + return result; + } + + const { cleanText } = this.valueToken; + + // warn user if specified glob pattern is not valid + if (this.isValidGlob(cleanText) === false) { + result.push( + new PromptMetadataWarning( + this.valueToken.range, + localize( + 'prompt.header.metadata.applyTo.diagnostics.non-valid-glob', + "Invalid glob pattern '{0}'.", + cleanText, + ), + ), + ); + + delete this.valueToken; + return result; + } + + return result; + } + + /** + * Check if a provided string contains a valid glob pattern. + */ + private isValidGlob( + pattern: string, + ): boolean { + try { + const globPattern = parse(pattern); + if (isEmptyPattern(globPattern)) { + return false; + } + + return true; + } catch (_error) { + return false; + } + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `applyTo`. + */ + public static isApplyToRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts new file mode 100644 index 00000000000..d36c92e48c7 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptStringMetadata } from './record.js'; +import { FrontMatterRecord, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the metadata record in the prompt header. + */ +const RECORD_NAME = 'description'; + +/** + * Prompt `description` metadata record inside the prompt header. + */ +export class PromptDescriptionMetadata extends PromptStringMetadata { + public override get recordName(): string { + return RECORD_NAME; + } + + constructor( + recordToken: FrontMatterRecord, + languageId: string, + ) { + super(RECORD_NAME, recordToken, languageId); + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `description`. + */ + public static isDescriptionRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts new file mode 100644 index 00000000000..7c7b1e44115 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/index.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { PromptModeMetadata } from './mode.js'; +export { PromptToolsMetadata } from './tools.js'; +export { PromptDescriptionMetadata } from './description.js'; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts new file mode 100644 index 00000000000..ad075b75c5c --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptStringMetadata } from './record.js'; +import { ChatMode } from '../../../../constants.js'; +import { localize } from '../../../../../../../../nls.js'; +import { PromptMetadataDiagnostic, PromptMetadataError } from '../diagnostics.js'; +import { FrontMatterRecord, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the metadata record in the prompt header. + */ +const RECORD_NAME = 'mode'; + +/** + * Valid chat mode values. + */ +const VALID_MODES = Object.freeze([ + ChatMode.Ask, + ChatMode.Edit, + ChatMode.Agent, +]); + +/** + * Prompt `mode` metadata record inside the prompt header. + */ +export class PromptModeMetadata extends PromptStringMetadata { + constructor( + recordToken: FrontMatterRecord, + languageId: string, + ) { + super(RECORD_NAME, recordToken, languageId); + } + + public override get recordName(): string { + return RECORD_NAME; + } + + /** + * Private field for tracking the chat mode value. + */ + private value: ChatMode | undefined; + /** + * Chat mode value of the metadata record. + */ + public get chatMode(): ChatMode | undefined { + return this.value; + } + + protected override validate(): readonly PromptMetadataDiagnostic[] { + const result: PromptMetadataDiagnostic[] = [ + ...super.validate(), + ]; + + if (this.text === undefined) { + return result; + } + + // validate that the text value is one of the valid modes + const validModes: string[] = [...VALID_MODES]; + const index = validModes.indexOf(this.text); + if (index !== -1) { + this.value = VALID_MODES[index]; + return result; + } + + // if not valid mode value, add an appropriate diagnostic + result.push( + new PromptMetadataError( + this.range, + localize( + 'prompt.header.metadata.mode.diagnostics.invalid-value', + "Value of the '{0}' metadata must be one of ({1}), got '{2}'.", + RECORD_NAME, + VALID_MODES + .map((modeName) => { + return `'${modeName}'`; + }).join(', '), + this.text, + ), + ), + ); + + return result; + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `mode`. + */ + public static isModeRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts new file mode 100644 index 00000000000..d0e779e7e4e --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../../../nls.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; +import { FrontMatterRecord, FrontMatterString } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Abstract class for all metadata records in the prompt header. + */ +export abstract class PromptMetadataRecord { + + /** + * Private field for tracking all diagnostic issues + * related to this metadata record. + */ + private readonly issues: PromptMetadataDiagnostic[]; + + /** + * Full range of the metadata's record text in the prompt header. + */ + public get range(): Range { + return this.recordToken.range; + } + + constructor( + protected readonly recordToken: FrontMatterRecord, + protected readonly languageId: string, + ) { + + this.issues = []; + this.issues.push(...this.validate()); + } + + /** + * Validate the metadata record and collect all issues + * related to its content. + */ + protected abstract validate(): readonly PromptMetadataDiagnostic[]; + + /** + * Name of the metadata record. + */ + public abstract get recordName(): string; + + /** + * List of all diagnostic issues related to this metadata record. + */ + public get diagnostics(): readonly PromptMetadataDiagnostic[] { + return this.issues; + } + + /** + * List of all `error` issue diagnostics. + */ + public get errorDiagnostics(): readonly PromptMetadataError[] { + return this.diagnostics + .filter((diagnostic) => { + return (diagnostic instanceof PromptMetadataError); + }); + } + + /** + * List of all `warning` issue diagnostics. + */ + public get warningDiagnostics(): readonly PromptMetadataWarning[] { + return this.diagnostics + .filter((diagnostic) => { + return (diagnostic instanceof PromptMetadataWarning); + }); + } +} + +/** + * Base class for all metadata records with a `string` value. + */ +export abstract class PromptStringMetadata extends PromptMetadataRecord { + /** + * Value token reference of the record. + */ + protected valueToken: FrontMatterString | undefined; + + /** + * Clean text value of the record. + */ + public get text(): string | undefined { + return this.valueToken?.cleanText; + } + + constructor( + expectedRecordName: string, + recordToken: FrontMatterRecord, + languageId: string, + ) { + // sanity check on the name of the record + const recordName = recordToken.nameToken.text; + assert( + recordName === expectedRecordName, + `Record token must be '${expectedRecordName}', got '${recordName}'.`, + ); + + super(recordToken, languageId); + } + + /** + * Validate the metadata record has a 'string' value. + */ + protected override validate(): readonly PromptMetadataDiagnostic[] { + const { valueToken } = this.recordToken; + + const result: PromptMetadataDiagnostic[] = []; + + // validate that the record value is a string + if ((valueToken instanceof FrontMatterString) === false) { + result.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.string.diagnostics.invalid-value-type', + "Value of the '{0}' metadata must be '{1}', got '{2}'.", + this.recordName, + 'string', + valueToken.valueTypeName, + ), + ), + ); + + return result; + } + + this.valueToken = valueToken; + return result; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts new file mode 100644 index 00000000000..fe5929f47ff --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptMetadataRecord } from './record.js'; +import { localize } from '../../../../../../../../nls.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; +import { FrontMatterArray, FrontMatterRecord, FrontMatterString, FrontMatterToken, FrontMatterValueToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the metadata record in the prompt header. + */ +const RECORD_NAME = 'tools'; + +/** + * Prompt `tools` metadata record inside the prompt header. + */ +export class PromptToolsMetadata extends PromptMetadataRecord { + public override get recordName(): string { + return RECORD_NAME; + } + + /** + * Value token reference of the record. + */ + protected valueToken: FrontMatterArray | undefined; + + /** + * List of all valid tool names that were found in + * this metadata record. + */ + private validToolNames: Set | undefined; + + /** + * List of all valid tool names that were found in + * this metadata record. + */ + public get toolNames(): readonly string[] { + if (this.validToolNames === undefined) { + return []; + } + + return [...this.validToolNames.values()]; + } + + constructor( + recordToken: FrontMatterRecord, + languageId: string, + ) { + // sanity check on the name of the tools record + assert( + PromptToolsMetadata.isToolsRecord(recordToken), + `Record token must be a tools token, got '${recordToken.nameToken.text}'.`, + ); + + super(recordToken, languageId); + } + + /** + * Validate the metadata record and collect all issues + * related to its content. + */ + protected override validate(): readonly PromptMetadataDiagnostic[] { + const result: PromptMetadataDiagnostic[] = []; + + const { valueToken } = this.recordToken; + + // validate that the record value is an array + if ((valueToken instanceof FrontMatterArray) === false) { + result.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.invalid-value-type', + "Value of the '{0}' metadata must be '{1}', got '{2}'.", + RECORD_NAME, + 'array', + valueToken.valueTypeName, + ), + ), + ); + + return result; + } + + this.valueToken = valueToken; + + // validate that all array items + this.validToolNames = new Set(); + for (const item of this.valueToken.items) { + result.push( + ...this.validateToolName(item, this.validToolNames), + ); + } + + return result; + } + + /** + * Validate an individual provided value token that + * is used for a tool name. + */ + private validateToolName( + valueToken: FrontMatterValueToken, + validToolNames: Set, + ): readonly PromptMetadataDiagnostic[] { + const issues: PromptMetadataDiagnostic[] = []; + + // tool name must be a string + if ((valueToken instanceof FrontMatterString) === false) { + issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.invalid-tool-name-type', + "Expected a tool name ({0}), got '{1}'.", + 'string', + valueToken.text, + ), + ), + ); + + return issues; + } + + const cleanToolName = valueToken.cleanText.trim(); + // the tool name should not be empty + if (cleanToolName.length === 0) { + issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.empty-tool-name', + "Tool name cannot be empty.", + ), + ), + ); + + return issues; + } + + // the tool name should not be duplicated + if (validToolNames.has(cleanToolName)) { + issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.duplicate-tool-name', + "Duplicate tool name '{0}'.", + cleanToolName, + ), + ), + ); + + return issues; + } + + validToolNames.add(cleanToolName); + return issues; + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `tools`. + */ + public static isToolsRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts new file mode 100644 index 00000000000..adca3da6a03 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { assertDefined } from '../../../../../../base/common/types.js'; +import { IPromptContentsProvider } from '../contentProviders/types.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { isUntitled } from '../../../../../../platform/prompts/common/constants.js'; +import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; +import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Get prompt contents provider object based on the prompt type. + */ +const getContentsProvider = ( + uri: URI, + options: Partial, + modelService: IModelService, + instaService: IInstantiationService, +): IPromptContentsProvider => { + // use text model contents provider for `untitled` documents + if (isUntitled(uri)) { + const model = modelService.getModel(uri); + + assertDefined( + model, + `Cannot find model of untitled document '${uri.path}'.`, + ); + + return instaService + .createInstance(TextModelContentsProvider, model, options); + } + + return instaService + .createInstance(FilePromptContentProvider, uri, options); +}; + +/** + * General prompt parser class that automatically infers a prompt + * contents provider type by the type of provided prompt URI. + */ +export class PromptParser extends BasePromptParser { + /** + * Underlying prompt contents provider instance. + */ + private readonly contentsProvider: IPromptContentsProvider; + + constructor( + uri: URI, + options: Partial = {}, + @ILogService logService: ILogService, + @IModelService modelService: IModelService, + @IInstantiationService instaService: IInstantiationService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, + ) { + const contentsProvider = getContentsProvider(uri, options, modelService, instaService); + + super( + contentsProvider, + options, + instaService, + workspaceService, + logService, + ); + + this.contentsProvider = this._register(contentsProvider); + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + const { sourceName } = this.contentsProvider; + + return `prompt-parser:${sourceName}:${this.uri.path}`; + } +} diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts index e0cf7edddac..72f21fd0a5e 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePromptParser } from './basePromptParser.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** @@ -16,13 +17,18 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ export class TextModelPromptParser extends BasePromptParser { constructor( model: ITextModel, - seenReferences: string[] = [], + options: Partial = {}, @IInstantiationService initService: IInstantiationService, + @IWorkspaceContextService workspaceService: IWorkspaceContextService, @ILogService logService: ILogService, ) { - const contentsProvider = initService.createInstance(TextModelContentsProvider, model); + const contentsProvider = initService.createInstance( + TextModelContentsProvider, + model, + options, + ); - super(contentsProvider, seenReferences, initService, logService); + super(contentsProvider, options, initService, workspaceService, logService); this._register(contentsProvider); } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts similarity index 85% rename from code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts rename to code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts index 096ada44af7..8da3a8abaa9 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ChatMode } from '../../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -48,6 +49,31 @@ export interface ITopError extends IResolveError { readonly localizedMessage: string; } +/** + * Metadata defined in the prompt header. + */ +export interface IPromptMetadata { + /** + * Description metadata in the prompt header. + */ + description?: string; + + /** + * Tools metadata in the prompt header. + */ + tools?: readonly string[]; + + /** + * Chat mode metadata in the prompt header. + */ + mode?: ChatMode; + + /** + * Chat 'applyTo' metadata in the prompt header. + */ + applyTo?: string; +} + /** * Base interface for a generic prompt reference. */ @@ -132,13 +158,13 @@ interface IPromptReferenceBase extends IDisposable { /** * Direct references of the current reference. */ - references: readonly IPromptReference[]; + readonly references: readonly IPromptReference[]; /** * All references that the current reference may have, * including all possible nested child references. */ - allReferences: readonly IPromptReference[]; + readonly allReferences: readonly IPromptReference[]; /** * All *valid* references that the current reference may have, @@ -148,7 +174,18 @@ interface IPromptReferenceBase extends IDisposable { * without creating a circular reference loop or having any other * issues that would make the reference resolve logic to fail. */ - allValidReferences: readonly IPromptReference[]; + readonly allValidReferences: readonly IPromptReference[]; + + /** + * Entire associated `tools` metadata for this reference and + * all possible nested child references. + */ + readonly allToolsMetadata: readonly string[] | null; + + /** + * Metadata defined in the prompt header. + */ + readonly metadata: IPromptMetadata; /** * Returns a promise that resolves when the reference contents diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 97297a4d720..ac10c061299 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -3,16 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptPath, IPromptsService } from './types.js'; +import { ChatMode } from '../../constants.js'; +import { localize } from '../../../../../../nls.js'; +import { PROMPT_LANGUAGE_ID } from '../constants.js'; +import { flatten, forEach } from '../utils/treeUtils.js'; +import { PromptParser } from '../parsers/promptParser.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { IPromptFileReference } from '../parsers/types.js'; +import { match } from '../../../../../../base/common/glob.js'; +import { pick } from '../../../../../../base/common/arrays.js'; import { assert } from '../../../../../../base/common/assert.js'; +import { basename } from '../../../../../../base/common/path.js'; +import { ResourceSet } from '../../../../../../base/common/map.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ObjectCache } from '../../../../../../base/common/objectCache.js'; import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; +import { IChatPromptSlashCommand, TCombinedToolsMetadata, IMetadata, IPromptPath, IPromptsService, TPromptsStorage, TPromptsType } from './types.js'; /** * Provides prompt services. @@ -28,18 +41,27 @@ export class PromptsService extends Disposable implements IPromptsService { /** * Prompt files locator utility. */ - private readonly fileLocator = this.initService.createInstance(PromptFilesLocator); + private readonly fileLocator: PromptFilesLocator; constructor( + @ILabelService private readonly labelService: ILabelService, + @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly initService: IInstantiationService, @IUserDataProfileService private readonly userDataService: IUserDataProfileService, ) { super(); + this.fileLocator = this.initService.createInstance(PromptFilesLocator); + // the factory function below creates a new prompt parser object // for the provided model, if no active non-disposed parser exists this.cache = this._register( new ObjectCache((model) => { + assert( + model.isDisposed() === false, + 'Text model must not be disposed.', + ); + /** * Note! When/if shared with "file" prompts, the `seenReferences` array below must be taken into account. * Otherwise consumers will either see incorrect failing or incorrect successful results, based on their @@ -48,10 +70,8 @@ export class PromptsService extends Disposable implements IPromptsService { const parser: TextModelPromptParser = initService.createInstance( TextModelPromptParser, model, - [], - ); - - parser.start(); + { seenReferences: [] }, + ).start(); // this is a sanity check and the contract of the object cache, // we must return a non-disposed object from this factory function @@ -74,52 +94,325 @@ export class PromptsService extends Disposable implements IPromptsService { model: ITextModel, ): TextModelPromptParser & { disposed: false } { assert( - !model.isDisposed(), + model.isDisposed() === false, 'Cannot create a prompt syntax parser for a disposed model.', ); return this.cache.get(model); } - public async listPromptFiles(): Promise { + public async listPromptFiles(type: TPromptsType): Promise { const userLocations = [this.userDataService.currentProfile.promptsHome]; const prompts = await Promise.all([ - this.fileLocator.listFilesIn(userLocations) - .then(withType('user')), - this.fileLocator.listFiles() - .then(withType('local')), + this.fileLocator.listFilesIn(userLocations, type) + .then(withType('user', type)), + this.fileLocator.listFiles(type) + .then(withType('local', type)), ]); return prompts.flat(); } - public getSourceFolders( - type: IPromptPath['type'], - ): readonly IPromptPath[] { + public getSourceFolders(type: TPromptsType): readonly IPromptPath[] { // sanity check to make sure we don't miss a new // prompt type that could be added in the future assert( - type === 'local' || type === 'user', + type === 'prompt' || type === 'instructions', `Unknown prompt type '${type}'.`, ); - const prompts = (type === 'user') - ? [this.userDataService.currentProfile.promptsHome] - : this.fileLocator.getConfigBasedSourceFolders(); + const result: IPromptPath[] = []; + + for (const uri of this.fileLocator.getConfigBasedSourceFolders(type)) { + result.push({ uri, storage: 'local', type }); + } + const userHome = this.userDataService.currentProfile.promptsHome; + result.push({ uri: userHome, storage: 'user', type }); + + return result; + } + + public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { + if (command.match(/^[\w_\-\.]+$/)) { + return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) }; + } + return undefined; + } + + public async resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise { + if (data.promptPath) { + return data.promptPath; + } + const files = await this.listPromptFiles('prompt'); + const command = data.command; + const result = files.find(file => getPromptCommandName(file.uri.path) === command); + if (result) { + return result; + } + const model = this.modelService.getModels().find(model => model.getLanguageId() === PROMPT_LANGUAGE_ID && getPromptCommandName(model.uri.path) === command); + if (model) { + return { uri: model.uri, storage: 'local', type: 'prompt' }; + } + return undefined; + } + + public async findPromptSlashCommands(): Promise { + const promptFiles = await this.listPromptFiles('prompt'); + return promptFiles.map(promptPath => { + const command = getPromptCommandName(promptPath.uri.path); + return { + command, + detail: localize('prompt.file.detail', 'Prompt file: {0}', this.labelService.getUriLabel(promptPath.uri, { relative: true })), + promptPath + }; + }); + } + + public async findInstructionFilesFor( + files: readonly URI[], + ): Promise { + const result: URI[] = []; + + const instructionFiles = await this.listPromptFiles('instructions'); + if (instructionFiles.length === 0) { + return result; + } + + const instructions = await this.getAllMetadata( + instructionFiles.map(pick('uri')), + ); + + for (const instruction of instructions.flatMap(flatten)) { + const { metadata, uri } = instruction; + const { applyTo } = metadata; + + if (applyTo === undefined) { + continue; + } + + // if glob pattern is one of the special wildcard values, + // add the instructions file event if no files are attached + if ((applyTo === '**') || (applyTo === '**/*')) { + result.push(uri); + + continue; + } + + // match each attached file with each glob pattern and + // add the instructions file if its rule matches the file + for (const file of files) { + if (match(applyTo, file.fsPath)) { + result.push(uri); + } + } + } + + return [...new ResourceSet(result)]; + } + + public async getAllMetadata( + promptUris: readonly URI[], + ): Promise { + const metadata = await Promise.all( + promptUris.map(async (uri) => { + let parser: PromptParser | undefined; + try { + parser = this.initService.createInstance( + PromptParser, + uri, + { allowNonPromptFiles: true }, + ).start(); + + await parser.allSettled(); + + return collectMetadata(parser); + } finally { + parser?.dispose(); + } + }), + ); + + return metadata; + } + + public async getCombinedToolsMetadata( + promptUris: readonly URI[], + ): Promise { + if (promptUris.length === 0) { + return null; + } + + const filesMetadata = await this.getAllMetadata(promptUris); + + const allTools = filesMetadata + .map((fileMetadata) => { + const result: string[] = []; + + let isFirst = true; + let isRootInAgentMode = false; + let hasTools = false; + + let chatMode: ChatMode | undefined; + + forEach((node) => { + const { metadata } = node; + const { mode, tools } = metadata; - return prompts.map(addType(type)); + if (isFirst === true) { + isFirst = false; + + if ((mode === ChatMode.Agent) || (tools !== undefined)) { + isRootInAgentMode = true; + + chatMode = ChatMode.Agent; + } + } + + chatMode ??= mode; + + // if both chat modes are set, pick the more privileged one + if (chatMode && mode) { + chatMode = morePrivilegedChatMode( + chatMode, + mode, + ); + } + + if (isRootInAgentMode && tools !== undefined) { + result.push(...tools); + hasTools = true; + } + + return false; + }, fileMetadata); + + if ((chatMode) === ChatMode.Agent) { + return { + tools: (hasTools) + ? [...new Set(result)] + : undefined, + mode: ChatMode.Agent, + }; + } + + return { + mode: chatMode, + }; + }); + + let hasAnyTools = false; + let resultingChatMode: ChatMode | undefined; + + const result: string[] = []; + for (const { tools, mode } of allTools) { + resultingChatMode ??= mode; + + // if both chat modes are set, pick the more privileged one + if (resultingChatMode && mode) { + resultingChatMode = morePrivilegedChatMode( + resultingChatMode, + mode, + ); + } + + if (tools) { + result.push(...tools); + hasAnyTools = true; + } + } + + if (resultingChatMode === ChatMode.Agent) { + return { + tools: (hasAnyTools) + ? [...new Set(result)] + : undefined, + mode: resultingChatMode, + }; + } + + return { + tools: undefined, + mode: resultingChatMode, + }; } } /** - * Utility to add a provided prompt `type` to a prompt URI. + * Pick a more privileged chat mode between two provided ones. + */ +const morePrivilegedChatMode = ( + chatMode1: ChatMode, + chatMode2: ChatMode, +): ChatMode => { + // when modes equal, return one of them + if (chatMode1 === chatMode2) { + return chatMode1; + } + + // when modes are different but one of them is 'agent', use 'agent' + if ((chatMode1 === ChatMode.Agent) || (chatMode2 === ChatMode.Agent)) { + return ChatMode.Agent; + } + + // when modes are different, none of them is 'agent', but one of them + // is 'edit', use 'edit' + if ((chatMode1 === ChatMode.Edit) || (chatMode2 === ChatMode.Edit)) { + return ChatMode.Edit; + } + + throw new Error( + [ + 'Invalid logic encountered: ', + `at this point modes '${chatMode1}' and '${chatMode2}' are different, but`, + `both must have be equal to '${ChatMode.Ask}' at the same time.`, + ].join(' '), + ); +}; + +/** + * Collect all metadata from prompt file references + * into a single hierarchical tree structure. + */ +const collectMetadata = ( + reference: Pick, +): IMetadata => { + const childMetadata = []; + for (const child of reference.references) { + if (child.errorCondition !== undefined) { + continue; + } + + childMetadata.push(collectMetadata(child)); + } + + const children = (childMetadata.length > 0) + ? childMetadata + : undefined; + + return { + uri: reference.uri, + metadata: reference.metadata, + children, + }; +}; + + +export function getPromptCommandName(path: string) { + const name = basename(path, PROMPT_FILE_EXTENSION); + return name; +} + +/** + * Utility to add a provided prompt `storage` and + * `type` attributes to a prompt URI. */ const addType = ( - type: 'local' | 'user', + storage: TPromptsStorage, + type: TPromptsType, ): (uri: URI) => IPromptPath => { return (uri) => { - return { uri, type: type }; + return { uri, storage, type }; }; }; @@ -127,10 +420,11 @@ const addType = ( * Utility to add a provided prompt `type` to a list of prompt URIs. */ const withType = ( - type: 'local' | 'user', + storage: TPromptsStorage, + type: TPromptsType, ): (uris: readonly URI[]) => (readonly IPromptPath[]) => { return (uris) => { return uris - .map(addType(type)); + .map(addType(storage, type)); }; }; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts index 1f29a5811b5..e9faf6a2f40 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TTree } from '../utils/treeUtils.js'; +import { ChatMode } from '../../constants.js'; +import { IPromptMetadata } from '../parsers/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; @@ -15,11 +18,14 @@ import { createDecorator } from '../../../../../../platform/instantiation/common export const IPromptsService = createDecorator('IPromptsService'); /** -* Supported prompt types. -* - `local` means the prompt is a local file. -* - `user` means a "roamble" prompt file (similar to snippets). -*/ -type TPromptsType = 'local' | 'user'; + * Where the prompt is stored. + */ +export type TPromptsStorage = 'local' | 'user'; + +/** + * What the prompt is used for. + */ +export type TPromptsType = 'instructions' | 'prompt'; /** * Represents a prompt path with its type. @@ -32,11 +38,84 @@ export interface IPromptPath { readonly uri: URI; /** - * Type of the prompt. + * Storage of the prompt. + */ + readonly storage: TPromptsStorage; + + /** + * Type of the prompt (e.g. 'prompt' or 'instructions'). */ readonly type: TPromptsType; } +/** + * Type for a shared prompt parser instance returned by the {@link IPromptsService}. + * Because the parser is shared, we omit the `dispose` method from + * the original type so the caller cannot dispose it prematurely + */ +export type TSharedPrompt = Omit; + +/** + * Metadata node object in a hierarchical tree of prompt references. + */ +export interface IMetadata { + /** + * URI of a prompt file. + */ + readonly uri: URI; + + /** + * Metadata of the prompt file. + */ + readonly metadata: IPromptMetadata; + + /** + * List of metadata for each valid child prompt reference. + */ + readonly children?: readonly TTree[]; +} + +/** + * Type of combined tools metadata for the case + * when the prompt is in the agent mode. + */ +interface ICombinedAgentToolsMetadata { + /** + * List of combined tools metadata for + * the entire tree of prompt references. + */ + readonly tools: readonly string[] | undefined; + + /** + * Resulting chat mode of a prompt, based on modes + * used in the entire tree of prompt references. + */ + readonly mode: ChatMode.Agent; +} + +/** + * Type of combined tools metadata for the case + * when the prompt is in non-agent mode. + */ +interface ICombinedNonAgentToolsMetadata { + /** + * List of combined tools metadata is empty + * when the prompt is in non-agent mode. + */ + readonly tools: undefined; + + /** + * Resulting chat mode of a prompt, based on modes + * used in the entire tree of prompt references. + */ + readonly mode?: ChatMode.Ask | ChatMode.Edit; +} + +/** + * General type of the combined tools metadata. + */ +export type TCombinedToolsMetadata = ICombinedAgentToolsMetadata | ICombinedNonAgentToolsMetadata; + /** * Provides prompt services. */ @@ -49,15 +128,74 @@ export interface IPromptsService extends IDisposable { */ getSyntaxParserFor( model: ITextModel, - ): TextModelPromptParser & { disposed: false }; + ): TSharedPrompt & { disposed: false }; /** * List all available prompt files. */ - listPromptFiles(): Promise; + listPromptFiles(type: TPromptsType): Promise; /** * Get a list of prompt source folders based on the provided prompt type. */ getSourceFolders(type: TPromptsType): readonly IPromptPath[]; + + /** + * Returns a prompt command if the command name. + * Undefined is returned if the name does not look like a file name of a prompt file. + */ + asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined; + + /** + * Gets the prompt file for a slash command. + */ + resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise; + + /** + * Returns a prompt command if the command name is valid. + */ + findPromptSlashCommands(): Promise; + + /** + * Find all instruction files which have a glob pattern in their + * 'applyTo' metadata record that match the provided list of files. + */ + findInstructionFilesFor( + fileUris: readonly URI[], + ): Promise; + + /** + * Get all metadata for entire prompt references tree + * that spans out of each of the provided files. + * + * In other words, the metadata tree is built starting from + * each of the provided files, therefore the result is a number + * of metadata trees, one for each file. + */ + getAllMetadata( + promptUris: readonly URI[], + ): Promise; + + /** + * Computes "combined" tools and chat mode metadata based on + * all provided files and their respective child references + * at the same time. + * + * For instance, the resulting {@link TCombinedToolsMetadata.mode} + * is computed as the least-privileged chat mode that can satisfy + * all the prompt files and their child references. + * + * On the other hand the resulting {@link TCombinedToolsMetadata.tools} + * metadata is computed as a union of all tools metadata that all + * prompt files and their child references specify. + */ + getCombinedToolsMetadata( + promptUris: readonly URI[], + ): Promise; +} + +export interface IChatPromptSlashCommand { + readonly command: string; + readonly detail: string; + readonly promptPath?: IPromptPath; } diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 975d45f56e2..7ed58095727 100644 --- a/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TPromptsType } from '../service/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { match } from '../../../../../../base/common/glob.js'; import { assert } from '../../../../../../base/common/assert.js'; @@ -13,7 +14,7 @@ import { PromptsConfig } from '../../../../../../platform/prompts/common/config. import { basename, dirname, extUri } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; +import { getPromptFileType, PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; /** * Utility class to locate prompt files. @@ -30,11 +31,11 @@ export class PromptFilesLocator { * * @returns List of prompt files found in the workspace. */ - public async listFiles(): Promise { - const configuredLocations = PromptsConfig.promptSourceFolders(this.configService); + public async listFiles(type: TPromptsType): Promise { + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); const absoluteLocations = toAbsoluteLocations(configuredLocations, this.workspaceService); - return await this.listFilesIn(absoluteLocations); + return await this.listFilesIn(absoluteLocations, type); } /** @@ -47,8 +48,9 @@ export class PromptFilesLocator { */ public async listFilesIn( folders: readonly URI[], + type: TPromptsType, ): Promise { - return await this.findInstructionFiles(folders); + return await this.findFilesInLocations(folders, type); } /** @@ -63,8 +65,8 @@ export class PromptFilesLocator { * * @returns List of possible unambiguous prompt file folders. */ - public getConfigBasedSourceFolders(): readonly URI[] { - const configuredLocations = PromptsConfig.promptSourceFolders(this.configService); + public getConfigBasedSourceFolders(type: TPromptsType): readonly URI[] { + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); const absoluteLocations = toAbsoluteLocations(configuredLocations, this.workspaceService); // locations in the settings can contain glob patterns so we need @@ -98,7 +100,7 @@ export class PromptFilesLocator { continue; } - result.add(URI.file(path)); + result.add(absoluteLocation.with({ path })); } return [...result]; @@ -112,9 +114,11 @@ export class PromptFilesLocator { * @param absoluteLocations List of prompt file source folders to search for prompt files in. Must be absolute paths. * @returns List of prompt files found in the provided source folders. */ - private async findInstructionFiles( + private async findFilesInLocations( absoluteLocations: readonly URI[], + type: TPromptsType, ): Promise { + // find all prompt files in the provided locations, then match // the found file paths against (possible) glob patterns const paths = new ResourceSet(); @@ -124,27 +128,34 @@ export class PromptFilesLocator { `Provided location must be an absolute path, got '${absoluteLocation.path}'.`, ); - // normalize the glob pattern to always end with "any prompt file" pattern - // unless the last part of the path is already a glob pattern itself; this is - // to handle the case when a user specifies a file glob pattern at the end, e.g., - // "my-folder/*.md" or "my-folder/*" already include the prompt files - const location = (isValidGlob(basename(absoluteLocation)) || absoluteLocation.path.endsWith(PROMPT_FILE_EXTENSION)) - ? absoluteLocation - : extUri.joinPath(absoluteLocation, `*${PROMPT_FILE_EXTENSION}`); - - // find all prompt files in entire file tree, starting from - // a first parent folder that does not contain a glob pattern - const promptFiles = await findAllPromptFiles( - firstNonGlobParent(location), - this.fileService, - ); - - // filter out found prompt files to only include those that match - // the original glob pattern specified in the settings (if any) - for (const file of promptFiles) { - if (match(location.path, file.path)) { + const nonGlobParent = firstNonGlobParent(absoluteLocation); + if (nonGlobParent === absoluteLocation) { + // the path does not contain a glob pattern, so we can + // just find all prompt files in the provided location + const promptFiles = await findFilesInLocation( + absoluteLocation, + type, + this.fileService, + ); + for (const file of promptFiles) { paths.add(file); } + } else { + // the path contains a glob pattern + // need to discuss whether to keep it or how to limit it (not documented yet) + const promptFiles = await findFilesInLocation( + nonGlobParent, + type, + this.fileService, + ); + + // filter out found prompt files to only include those that match + // the original glob pattern specified in the settings (if any) + for (const file of promptFiles) { + if (match(absoluteLocation.path, file.path)) { + paths.add(file); + } + } } } @@ -266,8 +277,9 @@ export const firstNonGlobParent = ( /** * Finds all `prompt files` in the provided location and all of its subfolders. */ -const findAllPromptFiles = async ( +const findFilesInLocation = async ( location: URI, + type: TPromptsType, fileService: IFileService, ): Promise => { const result: URI[] = []; @@ -275,7 +287,7 @@ const findAllPromptFiles = async ( try { const info = await fileService.resolve(location); - if (info.isFile && isPromptFile(info.resource)) { + if (info.isFile && getPromptFileType(info.resource) === type) { result.push(info.resource); return result; @@ -283,14 +295,14 @@ const findAllPromptFiles = async ( if (info.isDirectory && info.children) { for (const child of info.children) { - if (child.isFile && isPromptFile(child.resource)) { + if (child.isFile && getPromptFileType(child.resource) === type) { result.push(child.resource); continue; } if (child.isDirectory) { - const promptFiles = await findAllPromptFiles(child.resource, fileService); + const promptFiles = await findFilesInLocation(child.resource, type, fileService); result.push(...promptFiles); continue; diff --git a/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/treeUtils.ts b/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/treeUtils.ts new file mode 100644 index 00000000000..bfbc36075e0 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/common/promptSyntax/utils/treeUtils.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Type for a generic tree node. + */ +export type TTree = { children?: readonly TTree[] } & TTreenNode; + +/** + * Flatter a tree structure into a single flat array. + */ +export const flatten = ( + treeRoot: TTree, +): Omit[] => { + const result: Omit[] = []; + + result.push(treeRoot); + + for (const child of treeRoot.children ?? []) { + result.push(...flatten(child)); + } + + return result; +}; + +/** + * Traverse a tree structure and execute a callback for each node. + */ +export const forEach = ( + callback: (node: TTreeNode) => boolean, + treeRoot: TTree, +): ReturnType => { + const shouldStop = callback(treeRoot); + + if (shouldStop === true) { + return true; + } + + for (const child of treeRoot.children ?? []) { + const shouldStop = forEach(callback, child); + + if (shouldStop === true) { + return true; + } + } + + return false; +}; + +/** + * Maps nodes of a tree to a new type preserving the original tree structure by invoking + * the provided callback function for each node. + * + * @param callback Function to map each of the nodes in the tree. The callback receives the original + * readonly tree node and a list of its already-mapped readonly children and expected + * to return a new tree node object. If the new object does not have an explicit + * `children` property set (e.g., set to `undefined` or an array), the utility will + * automatically set the `children` property to the `new mapped children` for you, + * otherwise the set `children` property is preserved. Likewise, if the callback + * modifies the `newChildren` array directly, but doesn't explicitly set the `children` + * property on the returned object, the modification to the `newChildren` array are + * preserved in the resulting object. + * + * @param treeRoot The root node of the tree to be mapped. + * + * ### Examples + * + * ```typescript + * const tree = { + * id: '1', + * children: [ + * { id: '1.1' }, + * { id: '1.2' }, + * }; + * + * const newTree = map((node, _newChildren) => { + * return { + * name: `name-of-${node.id}`, + * }; + * }, tree); + * + * assert.deepStrictEqual(newTree, { + * name: 'name-of-1', + * children: [ + * { name: 'name-of-1.1' }, + * { name: 'name-of-1.2' }, + * }); + * ``` + */ +export const map = < + TTreeNode extends object, + TNewTreeNode extends object, +>( + callback: ( + originalNode: Readonly>, + newChildren: Readonly[] | undefined, + ) => TTree, + treeRoot: TTree, +): TTree => { + // if the node does not have children, just call the callback + if (treeRoot.children === undefined) { + return callback(treeRoot, undefined); + } + + // otherwise process all the children recursively first + const newChildren = treeRoot.children + .map(curry(map, callback)); + + // then run the callback with the new children + const newNode = callback(treeRoot, newChildren); + + // if user explicitly set the children, preserve the value + if ('children' in newNode) { + return newNode; + } + + // otherwise if no children is explicitly set, + // use the new children array instead + newNode.children = newChildren; + + return newNode; +}; + +/** + * Type for a rest parameters of function, excluding + * the first argument. + */ +type TRestParameters any> = + T extends (first: any, ...rest: infer R) => any ? R : never; + +/** + * Type for a curried function. + * See {@link curry} for more info. + */ +type TCurriedFunction any> = ((...args: TRestParameters) => ReturnType); + +/** + * Curry a provided function with the first argument. + */ +export const curry = ( + callback: (arg1: T, ...args: any[]) => K, + arg1: T, +): TCurriedFunction => { + return (...args) => { + return callback(arg1, ...args); + }; +}; diff --git a/code/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/code/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index c5fa196451a..42022cf5ca4 100644 --- a/code/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/code/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -7,108 +7,47 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { localize } from '../../../../../nls.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { SaveReason } from '../../../../common/editor.js'; -import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; -import { IToolInputProcessor } from './tools.js'; - -const codeInstructions = ` -The user is very smart and can understand how to apply your edits to their files, you just need to provide minimal hints. -Avoid repeating existing code, instead use comments to represent regions of unchanged code. The user prefers that you are as concise as possible. For example: -// ...existing code... -{ changed code } -// ...existing code... -{ changed code } -// ...existing code... - -Here is an example of how you should use format an edit to an existing Person class: -class Person { - // ...existing code... - age: number; - // ...existing code... - getAge() { - return this.age; - } -} -`; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../common/languageModelToolsService.js'; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; export const EditToolData: IToolData = { id: InternalEditToolId, - displayName: localize('chat.tools.editFile', "Edit File"), - modelDescription: `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, + displayName: '', // not used + modelDescription: '', // Not used source: { type: 'internal' }, - inputSchema: { - type: 'object', - properties: { - explanation: { - type: 'string', - description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', - }, - filePath: { - type: 'string', - description: 'An absolute path to the file to edit, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.', - }, - code: { - type: 'string', - description: 'The code change to apply to the file. ' + codeInstructions - } - }, - required: ['explanation', 'filePath', 'code'] - } }; +export interface EditToolParams { + uri: UriComponents; + explanation: string; + code: string; +} + export class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @ICodeMapperService private readonly codeMapperService: ICodeMapperService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, - @ITextFileService private readonly textFileService: ITextFileService, @INotebookService private readonly notebookService: INotebookService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, ) { } - async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { if (!invocation.context) { throw new Error('toolInvocationToken is required for this tool'); } const parameters = invocation.parameters as EditToolParams; - const fileUri = URI.revive(parameters.file); // TODO@roblourens do revive in MainThreadLanguageModelTools + const fileUri = URI.revive(parameters.uri); const uri = CellUri.parse(fileUri)?.notebook || fileUri; - if (!this.workspaceContextService.isInsideWorkspace(uri) && !this.notebookService.getNotebookTextModel(uri)) { - const groupsByLastActive = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); - const uriIsOpenInSomeEditor = groupsByLastActive.some((group) => { - return group.editors.some((editor) => { - return isEqual(editor.resource, uri); - }); - }); - - if (!uriIsOpenInSomeEditor) { - throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`); - } - } - - if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { - throw new Error(`File ${uri.fsPath} can't be edited because it is configured to be ignored by Copilot`); - } - const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; const request = model.getRequests().at(-1)!; @@ -133,7 +72,7 @@ export class EditTool implements IToolImpl { }); model.acceptResponseProgress(request, { kind: 'markdownContent', - content: new MarkdownString(parameters.code + '\n````\n') + content: new MarkdownString('\n````\n') }); // Signal start. if (this.notebookService.hasSupportedNotebooks(uri) && (this.notebookService.getNotebookTextModel(uri))) { @@ -158,7 +97,9 @@ export class EditTool implements IToolImpl { const result = await this.codeMapperService.mapCode({ codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], location: 'tool', - chatRequestId: invocation.chatRequestId + chatRequestId: invocation.chatRequestId, + chatRequestModel: invocation.modelId, + chatSessionId: invocation.context.sessionId, }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); @@ -201,11 +142,6 @@ export class EditTool implements IToolImpl { dispose.dispose(); }); - await this.textFileService.save(uri, { - reason: SaveReason.AUTO, - skipSaveParticipants: true, - }); - return { content: [{ kind: 'text', value: 'The file was edited successfully' }] }; @@ -217,31 +153,3 @@ export class EditTool implements IToolImpl { }; } } - -export interface EditToolParams { - file: UriComponents; - explanation: string; - code: string; -} - -export interface EditToolRawParams { - filePath: string; - explanation: string; - code: string; -} - -export class EditToolInputProcessor implements IToolInputProcessor { - processInput(input: EditToolRawParams): EditToolParams { - if (!input.filePath) { - // Tool name collision, or input wasn't properly validated upstream - return input as any; - } - const filePath = input.filePath; - // Runs in EH, will be mapped - return { - file: filePath.startsWith('untitled:') ? URI.parse(filePath) : URI.file(filePath), - explanation: input.explanation, - code: input.code, - }; - } -} diff --git a/code/src/vs/workbench/contrib/chat/common/tools/insertNotebookCellsTool.ts b/code/src/vs/workbench/contrib/chat/common/tools/insertNotebookCellsTool.ts deleted file mode 100644 index f623e892ced..00000000000 --- a/code/src/vs/workbench/contrib/chat/common/tools/insertNotebookCellsTool.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../../base/common/observable.js'; -import { URI, UriComponents } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; -import { localize } from '../../../../../nls.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { SaveReason } from '../../../../common/editor.js'; -import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import { CellUri } from '../../../notebook/common/notebookCommon.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ICodeMapperService } from '../chatCodeMapperService.js'; -import { IChatEditingService } from '../chatEditingService.js'; -import { ChatModel } from '../chatModel.js'; -import { IChatService } from '../chatService.js'; -import { ILanguageModelIgnoredFilesService } from '../ignoredFiles.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../languageModelToolsService.js'; -import { IToolInputProcessor } from './tools.js'; - -const codeInstructions = ` -The user is very smart and can understand how to insert cells to their new Notebook files -`; - -export const ExtensionEditToolId = 'vscode_insert_notebook_cells'; -export const InternalEditToolId = 'vscode_insert_notebook_cells_internal'; -export const EditToolData: IToolData = { - id: InternalEditToolId, - displayName: localize('chat.tools.editFile', "Edit File"), - modelDescription: `Insert cells into a new notebook n the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, - source: { type: 'internal' }, - inputSchema: { - type: 'object', - properties: { - explanation: { - type: 'string', - description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', - }, - filePath: { - type: 'string', - description: 'An absolute path to the file to edit, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.', - }, - cells: { - type: 'array', - description: 'The cells to insert to apply to the file. ' + codeInstructions - } - }, - required: ['explanation', 'filePath', 'code'] - } -}; - -export class EditTool implements IToolImpl { - - constructor( - @IChatService private readonly chatService: IChatService, - @IChatEditingService private readonly chatEditingService: IChatEditingService, - @ICodeMapperService private readonly codeMapperService: ICodeMapperService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, - @ITextFileService private readonly textFileService: ITextFileService, - @INotebookService private readonly notebookService: INotebookService, - ) { } - - async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { - if (!invocation.context) { - throw new Error('toolInvocationToken is required for this tool'); - } - - const parameters = invocation.parameters as EditToolParams; - const uri = URI.revive(parameters.file); // TODO@roblourens do revive in MainThreadLanguageModelTools - if (!this.workspaceContextService.isInsideWorkspace(uri)) { - throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`); - } - - if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { - throw new Error(`File ${uri.fsPath} can't be edited because it is configured to be ignored by Copilot`); - } - - const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; - const request = model.getRequests().at(-1)!; - - // Undo stops mark groups of response data in the output. Operations, such - // as text edits, that happen between undo stops are all done or undone together. - if (request.response?.response.getMarkdown().length) { - // slightly hacky way to avoid an extra 'no-op' undo stop at the start of responses that are just edits - model.acceptResponseProgress(request, { - kind: 'undoStop', - id: generateUuid(), - }); - } - - model.acceptResponseProgress(request, { - kind: 'markdownContent', - content: new MarkdownString('\n````\n') - }); - model.acceptResponseProgress(request, { - kind: 'codeblockUri', - uri - }); - model.acceptResponseProgress(request, { - kind: 'markdownContent', - content: new MarkdownString(parameters.code + '\n````\n') - }); - const notebookUri = CellUri.parse(uri)?.notebook || uri; - // Signal start. - if (this.notebookService.hasSupportedNotebooks(notebookUri) && (this.notebookService.getNotebookTextModel(notebookUri))) { - model.acceptResponseProgress(request, { - kind: 'notebookEdit', - edits: [], - uri: notebookUri - }); - } else { - model.acceptResponseProgress(request, { - kind: 'textEdit', - edits: [], - uri - }); - } - - const editSession = this.chatEditingService.getEditingSession(model.sessionId); - if (!editSession) { - throw new Error('This tool must be called from within an editing session'); - } - - const result = await this.codeMapperService.mapCode({ - codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], - location: 'tool', - chatRequestId: invocation.chatRequestId - }, { - textEdit: (target, edits) => { - model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); - }, - notebookEdit(target, edits) { - model.acceptResponseProgress(request, { kind: 'notebookEdit', uri: target, edits }); - }, - }, token); - - // Signal end. - if (this.notebookService.hasSupportedNotebooks(notebookUri) && (this.notebookService.getNotebookTextModel(notebookUri))) { - model.acceptResponseProgress(request, { kind: 'notebookEdit', uri: notebookUri, edits: [], done: true }); - } else { - model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); - } - - if (result?.errorMessage) { - throw new Error(result.errorMessage); - } - - let dispose: IDisposable; - await new Promise((resolve) => { - // The file will not be modified until the first edits start streaming in, - // so wait until we see that it _was_ modified before waiting for it to be done. - let wasFileBeingModified = false; - - dispose = autorun((r) => { - - const entries = editSession.entries.read(r); - const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); - if (currentFile) { - if (currentFile.isCurrentlyBeingModifiedBy.read(r)) { - wasFileBeingModified = true; - } else if (wasFileBeingModified) { - resolve(true); - } - } - }); - }).finally(() => { - dispose.dispose(); - }); - - await this.textFileService.save(uri, { - reason: SaveReason.AUTO, - skipSaveParticipants: true, - }); - - return { - content: [{ kind: 'text', value: 'The file was edited successfully' }] - }; - } - - async prepareToolInvocation(parameters: any, token: CancellationToken): Promise { - return { - presentation: 'hidden' - }; - } -} - -export interface EditToolParams { - file: UriComponents; - explanation: string; - code: string; -} - -export interface EditToolRawParams { - filePath: string; - explanation: string; - code: string; -} - -export class EditToolInputProcessor implements IToolInputProcessor { - processInput(input: EditToolRawParams): EditToolParams { - if (!input.filePath) { - // Tool name collision, or input wasn't properly validated upstream - return input as any; - } - const filePath = input.filePath; - // Runs in EH, will be mapped - return { - file: filePath.startsWith('untitled:') ? URI.parse(filePath) : URI.file(filePath), - explanation: input.explanation, - code: input.code, - }; - } -} diff --git a/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index 467b425090d..d3acab61bdc 100644 --- a/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/code/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -136,8 +136,6 @@ function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { return `${extensionIdentifier.value}/${toolName}`; } -const CopilotAgentModeTag = 'vscode_editing'; - export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; @@ -171,16 +169,8 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri continue; } - if (rawTool.tags?.includes(CopilotAgentModeTag)) { - if (!isProposedApiEnabled(extension.description, 'languageModelToolsForAgent') && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { - logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tag "${CopilotAgentModeTag}" without enabling 'languageModelToolsForAgent' proposal`); - continue; - } - } - - if (rawTool.tags?.some(tag => tag !== CopilotAgentModeTag && (tag.startsWith('copilot_') || tag.startsWith('vscode_'))) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`); - continue; } const rawIcon = rawTool.icon; @@ -203,16 +193,13 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri isProposedApiEnabled(extension.description, 'chatParticipantPrivate'); const tool: IToolData = { ...rawTool, - source: { type: 'extension', extensionId: extension.description.identifier, isExternalTool: !isBuiltinTool }, + source: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier, isExternalTool: !isBuiltinTool }, inputSchema: rawTool.inputSchema, id: rawTool.name, icon, when: rawTool.when ? ContextKeyExpr.deserialize(rawTool.when) : undefined, - requiresConfirmation: !isBuiltinTool, alwaysDisplayInputOutput: !isBuiltinTool, - supportsToolPicker: isBuiltinTool ? - false : - rawTool.canBeReferencedInPrompt + supportsToolPicker: rawTool.canBeReferencedInPrompt }; const disposable = languageModelToolsService.registerToolData(tool); this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable); diff --git a/code/src/vs/workbench/contrib/chat/common/tools/tools.ts b/code/src/vs/workbench/contrib/chat/common/tools/tools.ts index eac67b132eb..1ca0556cc94 100644 --- a/code/src/vs/workbench/contrib/chat/common/tools/tools.ts +++ b/code/src/vs/workbench/contrib/chat/common/tools/tools.ts @@ -25,8 +25,4 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo } } -export interface IToolInputProcessor { - processInput(input: any): any; -} - export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index c47f9b9ae2f..825f843715f 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -544,13 +544,13 @@ const primaryVoiceActionMenu = (when: ContextKeyExpression | undefined) => { return [ { id: MenuId.ChatInput, - when: ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), when), + when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), when), group: 'navigation', - order: 3 + order: 0 }, { id: MenuId.ChatExecute, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession).negate(), when), + when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel).negate(), when), group: 'navigation', order: 2 } @@ -736,7 +736,7 @@ class ChatSynthesizerSessions { const activeSession = this.activeSession = new CancellationTokenSource(); const disposables = new DisposableStore(); - activeSession.token.onCancellationRequested(() => disposables.dispose()); + disposables.add(activeSession.token.onCancellationRequested(() => disposables.dispose())); const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/code/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts index c8087d1521c..c037d3c729c 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../nls.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; -import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart } from '../../common/languageModelToolsService.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultTextPart, ToolProgress } from '../../common/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; export const FetchWebPageToolData: IToolData = { @@ -34,14 +34,13 @@ export const FetchWebPageToolData: IToolData = { }; export class FetchWebPageTool implements IToolImpl { - private _alreadyApprovedDomains = new Set(); + private _alreadyApprovedDomains = new ResourceSet(); constructor( @IWebContentExtractorService private readonly _readerModeService: IWebContentExtractorService, - @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, ) { } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _token: CancellationToken): Promise { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { const parsedUriResults = this._parseUris((invocation.parameters as { urls?: string[] }).urls); const validUris = Array.from(parsedUriResults.values()).filter((uri): uri is URI => !!uri); if (!validUris.length) { @@ -53,9 +52,7 @@ export class FetchWebPageTool implements IToolImpl { // We approved these via confirmation, so mark them as "approved" in this session // if they are not approved via the trusted domain service. for (const uri of validUris) { - if (!this._trustedDomainService.isValid(uri)) { - this._alreadyApprovedDomains.add(uri.toString(true)); - } + this._alreadyApprovedDomains.add(uri); } const contents = await this._readerModeService.extract(validUris); @@ -71,7 +68,11 @@ export class FetchWebPageTool implements IToolImpl { } }); - return { content: this._getPromptPartsForResults(contentsWithUndefined) }; + return { + content: this._getPromptPartsForResults(contentsWithUndefined), + // Have multiple results show in the dropdown + toolResultDetails: validUris.length > 1 ? validUris : undefined + }; } async prepareToolInvocation(parameters: any, token: CancellationToken): Promise { @@ -85,7 +86,7 @@ export class FetchWebPageTool implements IToolImpl { valid.push(uri); } }); - const urlsNeedingConfirmation = valid.filter(url => !this._trustedDomainService.isValid(url) && !this._alreadyApprovedDomains.has(url.toString(true))); + const urlsNeedingConfirmation = valid.filter(url => !this._alreadyApprovedDomains.has(url)); const pastTenseMessage = invalid.length ? invalid.length > 1 @@ -134,32 +135,17 @@ export class FetchWebPageTool implements IToolImpl { const result: IPreparedToolInvocation = { invocationMessage, pastTenseMessage }; if (urlsNeedingConfirmation.length) { - const confirmationTitle = urlsNeedingConfirmation.length > 1 - ? localize('fetchWebPage.confirmationTitle.plural', 'Fetch untrusted web pages?') - : localize('fetchWebPage.confirmationTitle.singular', 'Fetch untrusted web page?'); - - const managedTrustedDomainsCommand = 'workbench.action.manageTrustedDomain'; - const confirmationMessage = new MarkdownString( - urlsNeedingConfirmation.length > 1 - ? urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n') - : urlsNeedingConfirmation[0].toString(), - { - isTrusted: { enabledCommands: [managedTrustedDomainsCommand] }, - supportThemeIcons: true - } - ); - - confirmationMessage.appendMarkdown( - '\n\n$(info) ' + localize( - 'fetchWebPage.confirmationMessageManageTrustedDomains', - 'You can [manage your trusted domains]({0}) to skip this confirmation in the future.', - `command:${managedTrustedDomainsCommand}` - ) - ); - - result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage, allowAutoConfirm: false }; + let confirmationTitle: string; + let confirmationMessage: string | MarkdownString; + if (urlsNeedingConfirmation.length === 1) { + confirmationTitle = localize('fetchWebPage.confirmationTitle.singular', 'Fetch untrusted web page?'); + confirmationMessage = urlsNeedingConfirmation[0].toString(); + } else { + confirmationTitle = localize('fetchWebPage.confirmationTitle.plural', 'Fetch untrusted web pages?'); + confirmationMessage = new MarkdownString(urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n')); + } + result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage, allowAutoConfirm: true }; } - return result; } diff --git a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap index 67f63f14b70..7c307009368 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap @@ -1 +1 @@ -

<!--[CDATA[<div-->content]]>
\ No newline at end of file +

<!--[CDATA[<div-->content]]>

\ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap index 9def37d5acb..10d47923cbd 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap @@ -1 +1 @@ -
<!-- comment1 <div></div> -->
content
<!-- comment2 -->
\ No newline at end of file +
<!-- comment1 <div></div> -->
content

<!-- comment2 -->

\ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap index ba72307e533..fa56efb26d5 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap @@ -5,4 +5,4 @@
  • hi
  • </details> -
    <canvas>canvas here</canvas>
    <details></details> \ No newline at end of file +
    <canvas>canvas here</canvas>

    <details></details>

    \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap deleted file mode 100644 index 1241ef62b5f..00000000000 --- a/code/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap +++ /dev/null @@ -1 +0,0 @@ -

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

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

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

    \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts b/code/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts index f7612029ae7..17a7eec607b 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts @@ -17,7 +17,7 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { MockChatVariablesService } from '../common/mockChatVariables.js'; -import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; +import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; @@ -31,10 +31,11 @@ import { isEqual } from '../../../../../base/common/resources.js'; import { waitForState } from '../../../../../base/common/observable.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { ChatAgentLocation } from '../../common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; +import { ChatTransferService, IChatTransferService } from '../../common/chatTransferService.js'; -function getAgentData(id: string) { +function getAgentData(id: string): IChatAgentData { return { name: id, id: id, @@ -43,6 +44,7 @@ function getAgentData(id: string) { publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -62,6 +64,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() { @@ -114,7 +117,7 @@ suite('ChatEditingService', function () { test('create session', async function () { assert.ok(editingService); - const model = chatService.startSession(ChatAgentLocation.EditingSession, CancellationToken.None); + const model = chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); const session = await editingService.createEditingSession(model, true); assert.strictEqual(session.chatSessionId, model.sessionId); @@ -135,7 +138,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); - const model = chatService.startSession(ChatAgentLocation.EditingSession, CancellationToken.None); + const model = chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); const session = await model.editingSessionObs?.promise; if (!session) { assert.fail('session not created'); diff --git a/code/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts b/code/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts new file mode 100644 index 00000000000..cc036e2f6e4 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { cloneAndChange } from '../../../../../base/common/objects.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; +import { OffsetRange } from '../../../../../editor/common/core/offsetRange.js'; +import { FileService } from '../../../../../platform/files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { TestEnvironmentService } from '../../../../test/browser/workbenchTestServices.js'; +import { ISnapshotEntry } from '../../browser/chatEditing/chatEditingModifiedFileEntry.js'; +import { ChatEditingSessionStorage, IChatEditingSessionStop, StoredSessionState } from '../../browser/chatEditing/chatEditingSessionStorage.js'; +import { ChatEditingSnapshotTextModelContentProvider } from '../../browser/chatEditing/chatEditingTextModelContentProviders.js'; +import { ModifiedFileEntryState } from '../../common/chatEditingService.js'; + +suite('ChatEditingSessionStorage', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + const sessionId = generateUuid(); + let fs: FileService; + let storage: TestChatEditingSessionStorage; + + class TestChatEditingSessionStorage extends ChatEditingSessionStorage { + public get storageLocation() { + return super._getStorageLocation(); + } + } + + setup(() => { + fs = ds.add(new FileService(new NullLogService())); + ds.add(fs.registerProvider(TestEnvironmentService.workspaceStorageHome.scheme, ds.add(new InMemoryFileSystemProvider()))); + + storage = new TestChatEditingSessionStorage( + sessionId, + fs, + TestEnvironmentService, + new NullLogService(), + { getWorkspace: () => ({ id: 'workspaceId' }) } as any, + ); + }); + + function makeStop(requestId: string | undefined, before: string, after: string): IChatEditingSessionStop { + const stopId = generateUuid(); + const resource = URI.file('/foo.js'); + return { + stopId, + entries: new ResourceMap([ + [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, originalToCurrentEdit: OffsetEdit.replace(OffsetRange.ofLength(42), 'newtext'), state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId } } satisfies ISnapshotEntry], + ]), + }; + } + + function generateState(): StoredSessionState { + const initialFileContents = new ResourceMap(); + for (let i = 0; i < 10; i++) { initialFileContents.set(URI.file(`/foo${i}.js`), `fileContents${Math.floor(i / 2)}`); } + + const r1 = generateUuid(); + const r2 = generateUuid(); + return { + initialFileContents, + pendingSnapshot: makeStop(undefined, 'd', 'e'), + recentSnapshot: makeStop(undefined, 'd', 'e'), + linearHistoryIndex: 3, + linearHistory: [ + { startIndex: 0, requestId: r1, stops: [makeStop(r1, 'a', 'b')], postEdit: makeStop(r1, 'b', 'c').entries }, + { startIndex: 1, requestId: r2, stops: [makeStop(r2, 'c', 'd'), makeStop(r2, 'd', 'd')], postEdit: makeStop(r2, 'd', 'd').entries }, + ] + }; + } + + test('state is empty initially', async () => { + const s = await storage.restoreState(); + assert.strictEqual(s, undefined); + }); + + test('round trips state', async () => { + const original = generateState(); + await storage.storeState(original); + + const changer = (x: any) => { + return URI.isUri(x) ? x.toString() : x instanceof Map ? cloneAndChange([...x.values()], changer) : undefined; + }; + + const restored = await storage.restoreState(); + assert.deepStrictEqual(cloneAndChange(restored, changer), cloneAndChange(original, changer)); + }); + + test('clears state', async () => { + await storage.storeState(generateState()); + await storage.clearState(); + const s = await storage.restoreState(); + assert.strictEqual(s, undefined); + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/code/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts index 11e622395c4..8b12f3194a7 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -7,8 +7,6 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ChatMarkdownRenderer } from '../../browser/chatMarkdownRenderer.js'; -import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; -import { MockTrustedDomainService } from '../../../url/test/browser/mockTrustedDomainService.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; suite('ChatMarkdownRenderer', () => { @@ -17,7 +15,6 @@ suite('ChatMarkdownRenderer', () => { let testRenderer: ChatMarkdownRenderer; setup(() => { const instantiationService = store.add(workbenchInstantiationService(undefined, store)); - instantiationService.stub(ITrustedDomainService, new MockTrustedDomainService(['http://allowed.com'])); testRenderer = instantiationService.createInstance(ChatMarkdownRenderer, {}); }); @@ -102,8 +99,8 @@ suite('ChatMarkdownRenderer', () => { await assertSnapshot(result.element.outerHTML); }); - test('remote images', async () => { - const md = new MarkdownString(' '); + test('remote images are disallowed', async () => { + const md = new MarkdownString(''); md.supportHtml = true; const result = store.add(testRenderer.render(md)); await assertSnapshot(result.element.outerHTML); diff --git a/code/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/code/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 855c89aefed..5454f7f7ca3 100644 --- a/code/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -149,7 +149,7 @@ suite('LanguageModelToolsService', () => { const toolBarrier = new Barrier(); const toolImpl: IToolImpl = { - invoke: async (invocation, countTokens, cancelToken) => { + invoke: async (invocation, countTokens, progress, cancelToken) => { assert.strictEqual(invocation.callId, '1'); assert.strictEqual(invocation.toolId, 'testTool'); assert.deepStrictEqual(invocation.parameters, { a: 1 }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index a71a6928c26..c91b499d014 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -36,6 +36,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index 4ca12379473..4229538081b 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -36,6 +36,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index ca95c7e1cfa..062362603e4 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -22,6 +22,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index c062f9c138a..15b0d547d18 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -22,6 +22,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 22098b4e0de..eb55c494113 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -22,6 +22,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap index 27552be87ba..3f0b98575dd 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap @@ -22,6 +22,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap index 5617c8a0680..9d8283d8a07 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap @@ -22,6 +22,7 @@ extensionDisplayName: "", extensionPublisherId: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ { diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap new file mode 100644 index 00000000000..70b24f7309e --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap @@ -0,0 +1,33 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: " ", + kind: "text" + }, + { + range: { + start: 4, + endExclusive: 11 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 12 + }, + slashPromptCommand: { command: "prompt" }, + kind: "prompt" + } + ], + text: " /prompt" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap new file mode 100644 index 00000000000..ce48a80a13a --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 42 + }, + text: "/ route and the request of /search-option", + kind: "text" + } + ], + text: "/ route and the request of /search-option" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap new file mode 100644 index 00000000000..120c1cc46dd --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 52 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 53 + }, + text: "handle the / route and the request of /search-option", + kind: "text" + } + ], + text: "handle the / route and the request of /search-option" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap new file mode 100644 index 00000000000..b0f2a2c0d40 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap @@ -0,0 +1,33 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 4 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 5 + }, + text: " ", + kind: "text" + }, + { + range: { + start: 4, + endExclusive: 8 + }, + editorRange: { + startLineNumber: 1, + startColumn: 5, + endLineNumber: 1, + endColumn: 9 + }, + slashCommand: { command: "fix" }, + kind: "slash" + } + ], + text: " /fix" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap new file mode 100644 index 00000000000..b36f6191154 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 10 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 11 + }, + text: "Hello /fix", + kind: "text" + } + ], + text: "Hello /fix" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap new file mode 100644 index 00000000000..4a0dd5fb12d --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 65 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 66 + }, + text: "can we add a new file for an Express router to handle the / route", + kind: "text" + } + ], + text: "can we add a new file for an Express router to handle the / route" +} \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 2de45db43d1..954291a13fb 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -32,6 +32,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -74,6 +75,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -98,7 +100,9 @@ }, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index 0a9e4d67229..eac347edaac 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -32,6 +32,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -74,6 +75,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -82,7 +84,9 @@ usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 36fd2784d41..6c53d43dcbf 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -31,6 +31,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { requester: { name: "test" } }, slashCommands: [ ], disambiguation: [ ] @@ -81,6 +82,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { requester: { name: "test" } }, slashCommands: [ ], disambiguation: [ ] @@ -105,7 +107,9 @@ }, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined }, { requestId: undefined, @@ -148,6 +152,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { requester: { name: "test" } }, slashCommands: [ ], disambiguation: [ ], @@ -157,7 +162,9 @@ usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 8e5fd37cea2..f6a351338de 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/code/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -31,6 +31,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -74,6 +75,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], + modes: [ "ask" ], metadata: { }, slashCommands: [ ], disambiguation: [ ] @@ -82,7 +84,9 @@ usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index cec803118e1..41c00776359 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -18,6 +18,7 @@ const testAgentData: IChatAgentData = { extensionId: new ExtensionIdentifier(''), extensionPublisherId: '', locations: [], + modes: [], metadata: {}, slashCommands: [], disambiguation: [], diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index bd047108aea..a3b527ade6c 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -19,9 +19,10 @@ import { IChatService } from '../../common/chatService.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { ChatMode, ChatAgentLocation } from '../../common/constants.js'; -import { ILanguageModelToolsService, IToolData } from '../../common/languageModelToolsService.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; +import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { MockChatService } from './mockChatService.js'; -import { MockChatVariablesService } from './mockChatVariables.js'; +import { MockPromptsService } from './mockPromptsService.js'; suite('ChatRequestParser', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -29,7 +30,7 @@ suite('ChatRequestParser', () => { let instantiationService: TestInstantiationService; let parser: ChatRequestParser; - let toolsService: MockObject; + let variableService: MockObject; setup(async () => { instantiationService = testDisposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); @@ -37,11 +38,14 @@ suite('ChatRequestParser', () => { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); - instantiationService.stub(IChatVariablesService, new MockChatVariablesService()); instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IPromptsService, testDisposables.add(new MockPromptsService())); - toolsService = mockObject()({}); - instantiationService.stub(ILanguageModelToolsService, toolsService as any); + variableService = mockObject()(); + variableService.getDynamicVariables.returns([]); + variableService.getSelectedTools.returns([]); + + instantiationService.stub(IChatVariablesService, variableService as any); }); test('plain text', async () => { @@ -57,6 +61,13 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('slash in text', async () => { + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'can we add a new file for an Express router to handle the / route'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + test('slash command', async () => { const slashCommandService = mockObject()({}); slashCommandService.getCommands.returns([{ command: 'fix' }]); @@ -90,6 +101,89 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('slash command not first', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'Hello /fix'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('slash command after whitespace', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = ' /fix'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = ' /prompt'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command after text', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = 'handle the / route and the request of /search-option'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + test('prompt slash command after slash', async () => { + const slashCommandService = mockObject()({}); + slashCommandService.getCommands.returns([{ command: 'fix' }]); + instantiationService.stub(IChatSlashCommandService, slashCommandService as any); + + const promptSlashCommandService = mockObject()({}); + promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { + if (command.match(/^[\w_\-\.]+$/)) { + return { command }; + } + return undefined; + }); + instantiationService.stub(IPromptsService, promptSlashCommandService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const text = '/ route and the request of /search-option'; + const result = parser.parseChatRequest('1', text); + await assertSnapshot(result); + }); + + // test('variables', async () => { // varService.hasVariable.returns(true); // varService.getVariable.returns({ id: 'copilot.selection' }); @@ -120,7 +214,7 @@ suite('ChatRequestParser', () => { // }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, publisherDisplayName: '', extensionDisplayName: '', extensionPublisherId: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands, disambiguation: [] } satisfies IChatAgentData; + return { id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, publisherDisplayName: '', extensionDisplayName: '', extensionPublisherId: '', locations: [ChatAgentLocation.Panel], modes: [ChatMode.Ask], metadata: {}, slashCommands, disambiguation: [] } satisfies IChatAgentData; }; test('agent with subcommand after text', async () => { @@ -198,8 +292,10 @@ suite('ChatRequestParser', () => { agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); - toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); - toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); + variableService.getSelectedTools.returns([ + { id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }, + { id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } + ] satisfies IToolData[]); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole'); @@ -211,8 +307,10 @@ suite('ChatRequestParser', () => { agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); - toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); - toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); + variableService.getSelectedTools.returns([ + { id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }, + { id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } + ] satisfies IToolData[]); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole'); diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index be6d91e9c26..0d06d63cdfb 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { Event } from '../../../../../base/common/event.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -26,18 +27,19 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; +import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; -import { ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; +import { mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; +import { IChatEditingService, IChatEditingSession } from '../../common/chatEditingService.js'; import { IChatModel, ISerializableChatData } from '../../common/chatModel.js'; import { IChatFollowup, IChatService } from '../../common/chatService.js'; import { ChatService } from '../../common/chatServiceImpl.js'; import { ChatSlashCommandService, IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; -import { ChatAgentLocation } from '../../common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; import { MockChatService } from './mockChatService.js'; import { MockChatVariablesService } from './mockChatVariables.js'; -import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { @@ -48,6 +50,7 @@ const chatAgentWithUsedContext: IChatAgent = { extensionPublisherId: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -81,6 +84,7 @@ const chatAgentWithMarkdown: IChatAgent = { extensionPublisherId: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -93,7 +97,7 @@ const chatAgentWithMarkdown: IChatAgent = { }, }; -function getAgentData(id: string) { +function getAgentData(id: string): IChatAgentData { return { name: id, id: id, @@ -102,6 +106,7 @@ function getAgentData(id: string) { publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -133,6 +138,11 @@ suite('ChatService', () => { instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/path/to/workspaceStorage') }); instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None }); + instantiationService.stub(IChatEditingService, new class extends mock() { + override startOrContinueGlobalEditingSession(): Promise { + return Promise.resolve(Disposable.None as IChatEditingSession); + } + }); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); instantiationService.stub(IChatAgentService, chatAgentService); diff --git a/code/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/code/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 06560d694d6..138d176f95b 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -14,6 +14,7 @@ import { ChatMessageRole, IChatResponseFragment, languageModelExtensionPoint, La import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/modelPickerWidget.js'; suite('LanguageModels', function () { @@ -50,6 +51,7 @@ suite('LanguageModels', function () { name: 'Pretty Name', vendor: 'test-vendor', family: 'test-family', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, version: 'test-version', id: 'test-id', maxInputTokens: 100, @@ -70,6 +72,7 @@ suite('LanguageModels', function () { vendor: 'test-vendor', family: 'test2-family', version: 'test2-version', + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, id: 'test-id', maxInputTokens: 100, maxOutputTokens: 100, @@ -119,6 +122,7 @@ suite('LanguageModels', function () { id: 'actual-lm', maxInputTokens: 100, maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, }, sendChatRequest: async (messages, _from, _options, token) => { // const message = messages.at(-1); diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 57bc91080ed..346e678252d 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -12,6 +12,7 @@ import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequest import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatService implements IChatService { + edits2Enabled: boolean = false; _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; onDidSubmitRequest: Event<{ chatSessionId: string }> = Event.None; @@ -91,7 +92,6 @@ export class MockChatService implements IChatService { throw new Error('Method not implemented.'); } - unifiedViewEnabled = false; isEditingLocation(location: ChatAgentLocation): boolean { throw new Error('Method not implemented.'); } @@ -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/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 267af76a9f1..796050f1a23 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChatRequestVariableData, IChatRequestVariableEntry } from '../../common/chatModel.js'; -import { IParsedChatRequest } from '../../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; -import { ChatAgentLocation } from '../../common/constants.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; export class MockChatVariablesService implements IChatVariablesService { _serviceBrand: undefined; @@ -15,13 +13,7 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData { - return { - variables: [] - }; - } - - attachContext(name: string, value: unknown, location: ChatAgentLocation): void { - throw new Error('Method not implemented.'); + getSelectedTools(sessionId: string): readonly IToolData[] { + return []; } } diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 87a60b7fc0e..2cf7956aa5a 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { @@ -46,6 +47,10 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService return undefined; } + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { + + } + async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { return { content: [{ kind: 'text', value: 'result' }] diff --git a/code/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/code/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts new file mode 100644 index 00000000000..717b22e8b37 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/mockPromptsService.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 { URI } from '../../../../../base/common/uri.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js'; +import { IChatPromptSlashCommand, IMetadata, IPromptPath, IPromptsService, TCombinedToolsMetadata, TPromptsType } from '../../common/promptSyntax/service/types.js'; + +export class MockPromptsService implements IPromptsService { + getCombinedToolsMetadata(files: readonly URI[]): Promise { + throw new Error('Method not implemented.'); + } + getAllMetadata(files: readonly URI[]): Promise { + throw new Error('Method not implemented.'); + } + _serviceBrand: undefined; + getSyntaxParserFor(model: ITextModel): TextModelPromptParser & { disposed: false } { + throw new Error('Method not implemented.'); + } + listPromptFiles(type: TPromptsType): Promise { + throw new Error('Method not implemented.'); + } + getSourceFolders(type: TPromptsType): readonly IPromptPath[] { + throw new Error('Method not implemented.'); + } + public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { + return undefined; + } + resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise { + throw new Error('Method not implemented.'); + } + findPromptSlashCommands(): Promise { + throw new Error('Method not implemented.'); + } + findInstructionFilesFor(files: readonly URI[]): Promise { + throw new Error('Method not implemented.'); + } + dispose(): void { } +} diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts index b063215ec7d..83b44281913 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts @@ -9,6 +9,8 @@ import { newWriteableStream } from '../../../../../../../base/common/stream.js'; import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; import { ChatPromptCodec } from '../../../../common/promptSyntax/codecs/chatPromptCodec.js'; import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { Space, Tab, Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; @@ -51,30 +53,76 @@ suite('ChatPromptCodec', () => { await test.run( '#file:/etc/hosts some text\t\n for #file:./README.md\t testing\n ✔ purposes\n#file:LICENSE.md ✌ \t#file:.gitignore\n\n\n\t #file:/Users/legomushroom/repos/vscode \n\nsomething #file:\tsomewhere\n', [ + // reference new FileReference( new Range(1, 1, 1, 1 + 16), '/etc/hosts', ), + new Space(new Range(1, 17, 1, 17 + 1)), + new Word(new Range(1, 18, 1, 18 + 4), 'some'), + new Space(new Range(1, 22, 1, 22 + 1)), + new Word(new Range(1, 23, 1, 23 + 4), 'text'), + new Tab(new Range(1, 27, 1, 27 + 1)), + new NewLine(new Range(1, 28, 1, 28 + 1)), + new Space(new Range(2, 1, 2, 1 + 1)), + new Space(new Range(2, 2, 2, 2 + 1)), + new Word(new Range(2, 3, 2, 3 + 3), 'for'), + new Space(new Range(2, 6, 2, 6 + 1)), + // reference new FileReference( new Range(2, 7, 2, 7 + 17), './README.md', ), + new Tab(new Range(2, 24, 2, 24 + 1)), + new Space(new Range(2, 25, 2, 25 + 1)), + new Word(new Range(2, 26, 2, 26 + 7), 'testing'), + new NewLine(new Range(2, 33, 2, 33 + 1)), + new Space(new Range(3, 1, 3, 1 + 1)), + new Word(new Range(3, 2, 3, 2 + 1), '✔'), + new Space(new Range(3, 3, 3, 3 + 1)), + new Word(new Range(3, 4, 3, 4 + 8), 'purposes'), + new NewLine(new Range(3, 12, 3, 12 + 1)), + // reference new FileReference( new Range(4, 1, 4, 1 + 16), 'LICENSE.md', ), + new Space(new Range(4, 17, 4, 17 + 1)), + new Word(new Range(4, 18, 4, 18 + 1), '✌'), + new Space(new Range(4, 19, 4, 19 + 1)), + new Tab(new Range(4, 20, 4, 20 + 1)), + // reference new FileReference( new Range(4, 21, 4, 21 + 16), '.gitignore', ), + new NewLine(new Range(4, 37, 4, 37 + 1)), + new NewLine(new Range(5, 1, 5, 1 + 1)), + new NewLine(new Range(6, 1, 6, 1 + 1)), + new Tab(new Range(7, 1, 7, 1 + 1)), + new Space(new Range(7, 2, 7, 2 + 1)), + new Space(new Range(7, 3, 7, 3 + 1)), + new Space(new Range(7, 4, 7, 4 + 1)), + // reference new FileReference( new Range(7, 5, 7, 5 + 38), '/Users/legomushroom/repos/vscode', ), + new Space(new Range(7, 43, 7, 43 + 1)), + new Space(new Range(7, 44, 7, 44 + 1)), + new Space(new Range(7, 45, 7, 45 + 1)), + new NewLine(new Range(7, 46, 7, 46 + 1)), + new NewLine(new Range(8, 1, 8, 1 + 1)), + new Word(new Range(9, 1, 9, 1 + 9), 'something'), + new Space(new Range(9, 10, 9, 10 + 1)), + // reference new FileReference( new Range(9, 11, 9, 11 + 6), '', ), + new Tab(new Range(9, 17, 9, 17 + 1)), + new Word(new Range(9, 18, 9, 18 + 9), 'somewhere'), + new NewLine(new Range(9, 27, 9, 27 + 1)), ], ); }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts index 6f009d57468..9a36c891bb0 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts @@ -8,9 +8,15 @@ import { Range } from '../../../../../../../editor/common/core/range.js'; import { newWriteableStream } from '../../../../../../../base/common/stream.js'; import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; import { FileReference } from '../../../../common/promptSyntax/codecs/tokens/fileReference.js'; +import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { PromptAtMention } from '../../../../common/promptSyntax/codecs/tokens/promptAtMention.js'; +import { PromptSlashCommand } from '../../../../common/promptSyntax/codecs/tokens/promptSlashCommand.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { MarkdownLink } from '../../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; +import { PromptTemplateVariable } from '../../../../common/promptSyntax/codecs/tokens/promptTemplateVariable.js'; import { ChatPromptDecoder, TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; +import { PromptVariable, PromptVariableWithData } from '../../../../common/promptSyntax/codecs/tokens/promptVariable.js'; +import { At, Dash, ExclamationMark, FormFeed, Hash, Space, Tab, VerticalTab, Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; /** * A reusable test utility that asserts that a `ChatPromptDecoder` instance @@ -53,45 +59,365 @@ suite('ChatPromptDecoder', () => { const contents = [ '', - 'haalo!', + 'haalo! @workspace', ' message 👾 message #file:./path/to/file1.md', - '', + '\f', '## Heading Title', ' \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md', ' [#file:reference.md](./reference.md)some text #file:/some/file/with/absolute/path.md', - 'text text #file: another text', + 'text /run text #file: another @github text #selection even more text', + '\t\v#my-name:metadata:1:20 \t\t/command\t\v${inputs:id}\t', ]; await test.run( contents, [ + // first line + new NewLine(new Range(1, 1, 1, 2)), + // second line + new Word(new Range(2, 1, 2, 6), 'haalo'), + new ExclamationMark(new Range(2, 6, 2, 7)), + new Space(new Range(2, 7, 2, 8)), + new PromptAtMention( + new Range(2, 8, 2, 18), + 'workspace', + ), + new NewLine(new Range(2, 18, 2, 19)), + // third line + new Space(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 9), 'message'), + new Space(new Range(3, 9, 3, 10)), + new Word(new Range(3, 10, 3, 12), '👾'), + new Space(new Range(3, 12, 3, 13)), + new Word(new Range(3, 13, 3, 20), 'message'), + new Space(new Range(3, 20, 3, 21)), new FileReference( new Range(3, 21, 3, 21 + 24), './path/to/file1.md', ), + new NewLine(new Range(3, 45, 3, 46)), + // fourth line + new FormFeed(new Range(4, 1, 4, 2)), + new NewLine(new Range(4, 2, 4, 3)), + // fifth line + new Hash(new Range(5, 1, 5, 2)), + new Hash(new Range(5, 2, 5, 3)), + new Space(new Range(5, 3, 5, 4)), + new Word(new Range(5, 4, 5, 11), 'Heading'), + new Space(new Range(5, 11, 5, 12)), + new Word(new Range(5, 12, 5, 17), 'Title'), + new NewLine(new Range(5, 17, 5, 18)), + // sixth line + new Space(new Range(6, 1, 6, 2)), + new Tab(new Range(6, 2, 6, 3)), new FileReference( new Range(6, 3, 6, 3 + 24), 'a/b/c/filename2.md', ), + new Tab(new Range(6, 27, 6, 28)), + new Word(new Range(6, 28, 6, 30), '🖖'), + new Tab(new Range(6, 30, 6, 31)), new FileReference( new Range(6, 31, 6, 31 + 19), 'other-file.md', ), + new NewLine(new Range(6, 50, 6, 51)), + // seventh line + new Space(new Range(7, 1, 7, 2)), new MarkdownLink( 7, 2, '[#file:reference.md]', '(./reference.md)', ), + new Word(new Range(7, 38, 7, 38 + 4), 'some'), + new Space(new Range(7, 42, 7, 43)), + new Word(new Range(7, 43, 7, 43 + 4), 'text'), + new Space(new Range(7, 47, 7, 48)), new FileReference( new Range(7, 48, 7, 48 + 38), '/some/file/with/absolute/path.md', ), + new NewLine(new Range(7, 86, 7, 87)), + // eighth line + new Word(new Range(8, 1, 8, 5), 'text'), + new Space(new Range(8, 5, 8, 6)), + new PromptSlashCommand( + new Range(8, 6, 8, 6 + 4), + 'run', + ), + new Space(new Range(8, 10, 8, 11)), + new Word(new Range(8, 11, 8, 11 + 4), 'text'), + new Space(new Range(8, 15, 8, 16)), new FileReference( - new Range(8, 11, 8, 11 + 6), + new Range(8, 16, 8, 16 + 6), '', ), + new Space(new Range(8, 22, 8, 23)), + new Word(new Range(8, 23, 8, 23 + 7), 'another'), + new Space(new Range(8, 30, 8, 31)), + new PromptAtMention( + new Range(8, 31, 8, 32 + 6), + 'github', + ), + new Space(new Range(8, 38, 8, 39)), + new Word(new Range(8, 39, 8, 39 + 4), 'text'), + new Space(new Range(8, 43, 8, 44)), + new PromptVariable( + new Range(8, 44, 8, 44 + 10), + 'selection', + ), + new Space(new Range(8, 54, 8, 55)), + new Word(new Range(8, 55, 8, 55 + 4), 'even'), + new Space(new Range(8, 59, 8, 60)), + new Word(new Range(8, 60, 8, 60 + 4), 'more'), + new Space(new Range(8, 64, 8, 65)), + new Word(new Range(8, 65, 8, 65 + 4), 'text'), + new NewLine(new Range(8, 69, 8, 70)), + // ninth line + new Tab(new Range(9, 1, 9, 2)), + new VerticalTab(new Range(9, 2, 9, 3)), + new PromptVariableWithData( + new Range(9, 3, 9, 3 + 22), + 'my-name', + 'metadata:1:20', + ), + new Space(new Range(9, 25, 9, 26)), + new Tab(new Range(9, 26, 9, 27)), + new Tab(new Range(9, 27, 9, 28)), + new PromptSlashCommand( + new Range(9, 28, 9, 28 + 8), + 'command', + ), + new Tab(new Range(9, 36, 9, 37)), + new VerticalTab(new Range(9, 37, 9, 38)), + new PromptTemplateVariable( + new Range(9, 38, 9, 38 + 12), + 'inputs:id', + ), + new Tab(new Range(9, 50, 9, 51)), ], ); }); + + suite('• variables', () => { + test('• produces expected tokens', async () => { + const test = testDisposables.add( + new TestChatPromptDecoder(), + ); + + const contents = [ + '', + '\t\v#variable@', + ' #selection#your-variable', + 'some-text #var:12-67# some text', + ]; + + await test.run( + contents, + [ + // first line + new NewLine(new Range(1, 1, 1, 2)), + // second line + new Tab(new Range(2, 1, 2, 2)), + new VerticalTab(new Range(2, 2, 2, 3)), + new PromptVariable( + new Range(2, 3, 2, 3 + 9), + 'variable', + ), + new At(new Range(2, 12, 2, 13)), + new NewLine(new Range(2, 13, 2, 14)), + // third line + new Space(new Range(3, 1, 3, 2)), + new PromptVariable( + new Range(3, 2, 3, 2 + 10), + 'selection', + ), + new PromptVariable( + new Range(3, 12, 3, 12 + 14), + 'your-variable', + ), + new NewLine(new Range(3, 26, 3, 27)), + // forth line + new Word(new Range(4, 1, 4, 5), 'some'), + new Dash(new Range(4, 5, 4, 6)), + new Word(new Range(4, 6, 4, 6 + 4), 'text'), + new Space(new Range(4, 10, 4, 11)), + new PromptVariableWithData( + new Range(4, 11, 4, 11 + 10), + 'var', + '12-67', + ), + new Hash(new Range(4, 21, 4, 22)), + new Space(new Range(4, 22, 4, 23)), + new Word(new Range(4, 23, 4, 23 + 4), 'some'), + new Space(new Range(4, 27, 4, 28)), + new Word(new Range(4, 28, 4, 28 + 4), 'text'), + ], + ); + }); + }); + + suite('• commands', () => { + test('• produces expected tokens', async () => { + const test = testDisposables.add( + new TestChatPromptDecoder(), + ); + + const contents = [ + 'my command is \t/run', + 'your /command\v is done', + '/their#command is a pun', + 'and the /none@cmd was made by a nun', + ]; + + await test.run( + contents, + [ + // first line + new Word(new Range(1, 1, 1, 3), 'my'), + new Space(new Range(1, 3, 1, 4)), + new Word(new Range(1, 4, 1, 11), 'command'), + new Space(new Range(1, 11, 1, 12)), + new Word(new Range(1, 12, 1, 12 + 2), 'is'), + new Space(new Range(1, 14, 1, 15)), + new Tab(new Range(1, 15, 1, 16)), + new PromptSlashCommand( + new Range(1, 16, 1, 16 + 4), + 'run', + ), + new NewLine(new Range(1, 20, 1, 21)), + // second line + new Word(new Range(2, 1, 2, 5), 'your'), + new Space(new Range(2, 5, 2, 6)), + new PromptSlashCommand( + new Range(2, 6, 2, 6 + 8), + 'command', + ), + new VerticalTab(new Range(2, 14, 2, 15)), + new Space(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 2), 'is'), + new Space(new Range(2, 18, 2, 19)), + new Word(new Range(2, 19, 2, 19 + 4), 'done'), + new NewLine(new Range(2, 23, 2, 24)), + // third line + new PromptSlashCommand( + new Range(3, 1, 3, 1 + 6), + 'their', + ), + new PromptVariable( + new Range(3, 7, 3, 7 + 8), + 'command', + ), + new Space(new Range(3, 15, 3, 16)), + new Word(new Range(3, 16, 3, 16 + 2), 'is'), + new Space(new Range(3, 18, 3, 19)), + new Space(new Range(3, 19, 3, 20)), + new Word(new Range(3, 20, 3, 20 + 1), 'a'), + new Space(new Range(3, 21, 3, 22)), + new Word(new Range(3, 22, 3, 22 + 3), 'pun'), + new NewLine(new Range(3, 25, 3, 26)), + // forth line + new Word(new Range(4, 1, 4, 4), 'and'), + new Space(new Range(4, 4, 4, 5)), + new Word(new Range(4, 5, 4, 5 + 3), 'the'), + new Space(new Range(4, 8, 4, 9)), + new PromptSlashCommand( + new Range(4, 9, 4, 9 + 5), + 'none', + ), + new PromptAtMention( + new Range(4, 14, 4, 14 + 4), + 'cmd', + ), + new Space(new Range(4, 18, 4, 19)), + new Word(new Range(4, 19, 4, 19 + 3), 'was'), + new Space(new Range(4, 22, 4, 23)), + new Word(new Range(4, 23, 4, 23 + 4), 'made'), + new Space(new Range(4, 27, 4, 28)), + new Word(new Range(4, 28, 4, 28 + 2), 'by'), + new Space(new Range(4, 30, 4, 31)), + new Word(new Range(4, 31, 4, 31 + 1), 'a'), + new Space(new Range(4, 32, 4, 33)), + new Word(new Range(4, 33, 4, 33 + 3), 'nun'), + ], + ); + }); + }); + + suite('• template variables', () => { + test('• produces expected tokens', async () => { + const test = testDisposables.add( + new TestChatPromptDecoder(), + ); + + const contents = [ + 'my command is \t${run}', + 'your ${variable}\v is done', + '${their:variable} is a pun', + 'and the ${none:var} is made for fun', + ]; + + await test.run( + contents, + [ + // first line + new Word(new Range(1, 1, 1, 3), 'my'), + new Space(new Range(1, 3, 1, 4)), + new Word(new Range(1, 4, 1, 11), 'command'), + new Space(new Range(1, 11, 1, 12)), + new Word(new Range(1, 12, 1, 12 + 2), 'is'), + new Space(new Range(1, 14, 1, 15)), + new Tab(new Range(1, 15, 1, 16)), + new PromptTemplateVariable( + new Range(1, 16, 1, 16 + 6), + 'run', + ), + new NewLine(new Range(1, 22, 1, 23)), + // second line + new Word(new Range(2, 1, 2, 5), 'your'), + new Space(new Range(2, 5, 2, 6)), + new PromptTemplateVariable( + new Range(2, 6, 2, 6 + 11), + 'variable', + ), + new VerticalTab(new Range(2, 17, 2, 18)), + new Space(new Range(2, 18, 2, 19)), + new Word(new Range(2, 19, 2, 19 + 2), 'is'), + new Space(new Range(2, 21, 2, 22)), + new Word(new Range(2, 22, 2, 22 + 4), 'done'), + new NewLine(new Range(2, 26, 2, 27)), + // third line + new PromptTemplateVariable( + new Range(3, 1, 3, 1 + 17), + 'their:variable', + ), + new Space(new Range(3, 18, 3, 19)), + new Word(new Range(3, 19, 3, 19 + 2), 'is'), + new Space(new Range(3, 21, 3, 22)), + new Word(new Range(3, 22, 3, 22 + 1), 'a'), + new Space(new Range(3, 23, 3, 24)), + new Word(new Range(3, 24, 3, 24 + 3), 'pun'), + new NewLine(new Range(3, 27, 3, 28)), + // forth line + new Word(new Range(4, 1, 4, 4), 'and'), + new Space(new Range(4, 4, 4, 5)), + new Word(new Range(4, 5, 4, 5 + 3), 'the'), + new Space(new Range(4, 8, 4, 9)), + new PromptTemplateVariable( + new Range(4, 9, 4, 9 + 11), + 'none:var', + ), + new Space(new Range(4, 20, 4, 21)), + new Word(new Range(4, 21, 4, 21 + 2), 'is'), + new Space(new Range(4, 23, 4, 24)), + new Word(new Range(4, 24, 4, 24 + 4), 'made'), + new Space(new Range(4, 28, 4, 29)), + new Word(new Range(4, 29, 4, 29 + 3), 'for'), + new Space(new Range(4, 32, 4, 33)), + new Word(new Range(4, 33, 4, 33 + 3), 'fun'), + ], + ); + }); + }); }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts new file mode 100644 index 00000000000..7eadafaefb6 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts @@ -0,0 +1,445 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../../base/common/assert.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { randomInt } from '../../../../../../../base/common/numbers.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { Text } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { newWriteableStream } from '../../../../../../../base/common/stream.js'; +import { randomBoolean } from '../../../../../../../base/test/common/testUtils.js'; +import { TestDecoder } from '../../../../../../../editor/test/common/utils/testDecoder.js'; +import { Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; +import { TChatPromptToken } from '../../../../common/promptSyntax/codecs/chatPromptDecoder.js'; +import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; +import { TestSimpleDecoder } from '../../../../../../../editor/test/common/codecs/simpleDecoder.test.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; +import { FrontMatterHeader } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { Colon, Dash, DoubleQuote, Space, Tab, VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; +import { MarkdownExtensionsDecoder } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.js'; +import { FrontMatterMarker, TMarkerToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; + +/** + * Type for supported end-of-line tokens. + */ +type TEndOfLine = '\n' | '\r\n'; + +/** + * End-of-line utility class for convenience. + */ +class TestEndOfLine extends Text { + /** + * Create a new instance with provided end-of line type and + * a starting position. + */ + public static create( + type: TEndOfLine, + lineNumber: number, + startColumn: number, + ): TestEndOfLine { + // sanity checks + assert( + lineNumber >= 1, + `Line number must be greater than or equal to 1, got '${lineNumber}'.`, + ); + assert( + startColumn >= 1, + `Start column must be greater than or equal to 1, got '${startColumn}'.`, + ); + + const tokens = []; + + if (type === '\r\n') { + tokens.push(new CarriageReturn( + new Range( + lineNumber, + startColumn, + lineNumber, + startColumn + 1, + ), + )); + + startColumn += 1; + } + + tokens.push(new NewLine( + new Range( + lineNumber, + startColumn, + lineNumber, + startColumn + 1, + ), + )); + + return TestEndOfLine.fromTokens(tokens); + } +} + +/** + * Test decoder for the `MarkdownExtensionsDecoder` class. + */ +export class TestMarkdownExtensionsDecoder extends TestDecoder { + constructor( + ) { + const stream = newWriteableStream(null); + const decoder = new MarkdownExtensionsDecoder(stream); + + super(stream, decoder); + } +} + +/** + * Front Matter marker utility class for testing purposes. + */ +class TestFrontMatterMarker extends FrontMatterMarker { + /** + * Create a new instance with provided dashes count, + * line number, and an end-of-line type. + */ + public static create( + dashCount: number, + lineNumber: number, + endOfLine?: TEndOfLine | undefined, + ): TestFrontMatterMarker { + const tokens: TMarkerToken[] = []; + + let columnNumber = 1; + while (columnNumber <= dashCount) { + tokens.push(new Dash( + new Range( + lineNumber, + columnNumber, + lineNumber, + columnNumber + 1, + ), + )); + + columnNumber++; + } + + if (endOfLine !== undefined) { + const endOfLineTokens = TestEndOfLine.create( + endOfLine, + lineNumber, + columnNumber, + ); + tokens.push(...endOfLineTokens.tokens); + } + + return TestFrontMatterMarker.fromTokens(tokens); + } +} + +suite('MarkdownExtensionsDecoder', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + /** + * Create a Front Matter header start/end marker with a random length. + */ + const randomMarker = ( + maxDashCount: number = 10, + minDashCount: number = 1, + ): string => { + const dashCount = randomInt(maxDashCount, minDashCount); + + return new Array(dashCount).fill('-').join(''); + }; + + suite('• Front Matter header', () => { + suite('• successful cases', () => { + test('• produces expected tokens', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + // both line endings should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const markerLength = randomInt(10, 3); + + const promptContents = [ + new Array(markerLength).fill('-').join(''), + 'variables: ', + ' - name: value\v', + new Array(markerLength).fill('-').join(''), + 'some text', + ]; + + const startMarker = TestFrontMatterMarker.create(markerLength, 1, newLine); + const endMarker = TestFrontMatterMarker.create(markerLength, 4, newLine); + + await test.run( + promptContents.join(newLine), + [ + // header + new FrontMatterHeader( + new Range(1, 1, 4, 1 + markerLength + newLine.length), + startMarker, + Text.fromTokens([ + new Word(new Range(2, 1, 2, 1 + 9), 'variables'), + new Colon(new Range(2, 10, 2, 11)), + new Space(new Range(2, 11, 2, 12)), + ...TestEndOfLine.create(newLine, 2, 12).tokens, + new Space(new Range(3, 1, 3, 2)), + new Space(new Range(3, 2, 3, 3)), + new Dash(new Range(3, 3, 3, 4)), + new Space(new Range(3, 4, 3, 5)), + new Word(new Range(3, 5, 3, 5 + 4), 'name'), + new Colon(new Range(3, 9, 3, 10)), + new Space(new Range(3, 10, 3, 11)), + new Word(new Range(3, 11, 3, 11 + 5), 'value'), + new VerticalTab(new Range(3, 16, 3, 17)), + ...TestEndOfLine.create(newLine, 3, 17).tokens, + ]), + endMarker, + ), + // content after the header + new Word(new Range(5, 1, 5, 1 + 4), 'some'), + new Space(new Range(5, 5, 5, 6)), + new Word(new Range(5, 6, 5, 6 + 4), 'text'), + ], + ); + }); + + test('• can contain dashes in the header contents', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + // both line endings should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const markerLength = randomInt(10, 4); + + // number of dashes inside the header contents it should not matter how many + // dashes are there, but the count should not be equal to `markerLength` + const dashesLength = (randomBoolean()) + ? randomInt(markerLength - 1, 1) + : randomInt(2 * markerLength, markerLength + 1); + + const promptContents = [ + // start marker + new Array(markerLength).fill('-').join(''), + // contents + 'variables: ', + new Array(dashesLength).fill('-').join(''), // dashes inside the contents + ' - name: value\t', + // end marker + new Array(markerLength).fill('-').join(''), + 'some text', + ]; + + const startMarker = TestFrontMatterMarker.create(markerLength, 1, newLine); + const endMarker = TestFrontMatterMarker.create(markerLength, 4, newLine); + + await test.run( + promptContents.join(newLine), + [ + // header + new FrontMatterHeader( + new Range(1, 1, 5, 1 + markerLength + newLine.length), + startMarker, + Text.fromTokens([ + new Word(new Range(2, 1, 2, 1 + 9), 'variables'), + new Colon(new Range(2, 10, 2, 11)), + new Space(new Range(2, 11, 2, 12)), + ...TestEndOfLine.create(newLine, 2, 12).tokens, + // dashes inside the header + ...TestFrontMatterMarker.create(dashesLength, 3, newLine).dashTokens, + ...TestEndOfLine.create(newLine, 3, dashesLength + 1).tokens, + // - + new Space(new Range(4, 1, 4, 2)), + new Space(new Range(4, 2, 4, 3)), + new Dash(new Range(4, 3, 4, 4)), + new Space(new Range(4, 4, 4, 5)), + new Word(new Range(4, 5, 4, 5 + 4), 'name'), + new Colon(new Range(4, 9, 4, 10)), + new Space(new Range(4, 10, 4, 11)), + new Word(new Range(4, 11, 4, 11 + 5), 'value'), + new Tab(new Range(4, 16, 4, 17)), + ...TestEndOfLine.create(newLine, 4, 17).tokens, + ]), + endMarker, + ), + // content after the header + new Word(new Range(6, 1, 6, 1 + 4), 'some'), + new Space(new Range(6, 5, 6, 6)), + new Word(new Range(6, 6, 6, 6 + 4), 'text'), + ], + ); + }); + + test('• can be at the end of the file', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + // both line endings should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const markerLength = randomInt(10, 4); + + const promptContents = [ + // start marker + new Array(markerLength).fill('-').join(''), + // contents + ' description: "my description"', + // end marker + new Array(markerLength).fill('-').join(''), + ]; + + const startMarker = TestFrontMatterMarker.create(markerLength, 1, newLine); + const endMarker = TestFrontMatterMarker.create(markerLength, 3); + + await test.run( + promptContents.join(newLine), + [ + // header + new FrontMatterHeader( + new Range(1, 1, 3, 1 + markerLength), + startMarker, + Text.fromTokens([ + new Tab(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 11), 'description'), + new Colon(new Range(2, 13, 2, 14)), + new Space(new Range(2, 14, 2, 15)), + new DoubleQuote(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 2), 'my'), + new Space(new Range(2, 18, 2, 19)), + new Word(new Range(2, 19, 2, 19 + 11), 'description'), + new DoubleQuote(new Range(2, 30, 2, 31)), + ...TestEndOfLine.create(newLine, 2, 31).tokens, + ]), + endMarker, + ), + ], + ); + }); + }); + + suite('• failure cases', () => { + test('• fails if header starts not on the first line', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + const simpleDecoder = disposables.add( + new TestSimpleDecoder(), + ); + + const marker = randomMarker(5); + + // prompt contents + const contents = [ + '', + marker, + 'variables:', + ' - name: value', + marker, + 'some text', + ]; + + // both line ending should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const stringContents = contents.join(newLine); + + // send the same contents to the simple decoder + simpleDecoder.sendData(stringContents); + + // in the failure case we expect tokens to be re-emitted, therefore + // the list of tokens produced must be equal to the one of SimpleDecoder + await test.run( + stringContents, + (await simpleDecoder.receiveTokens()), + ); + }); + + test('• fails if header markers do not match (start marker is longer)', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + const simpleDecoder = disposables.add( + new TestSimpleDecoder(), + ); + + const marker = randomMarker(5); + + // prompt contents + const contents = [ + `${marker}${marker}`, + 'variables:', + ' - name: value', + marker, + 'some text', + ]; + + // both line ending should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const stringContents = contents.join(newLine); + + // send the same contents to the simple decoder + simpleDecoder.sendData(stringContents); + + // in the failure case we expect tokens to be re-emitted, therefore + // the list of tokens produced must be equal to the one of SimpleDecoder + await test.run( + stringContents, + (await simpleDecoder.receiveTokens()), + ); + }); + + test('• fails if header markers do not match (end marker is longer)', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + const simpleDecoder = disposables.add( + new TestSimpleDecoder(), + ); + + const marker = randomMarker(5); + + const promptContents = [ + marker, + 'variables:', + ' - name: value', + `${marker}${marker}`, + 'some text', + ]; + + // both line ending should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const stringContents = promptContents.join(newLine); + + // send the same contents to the simple decoder + simpleDecoder.sendData(stringContents); + + // in the failure case we expect tokens to be re-emitted, therefore + // the list of tokens produced must be equal to the one of SimpleDecoder + await test.run( + stringContents, + (await simpleDecoder.receiveTokens()), + ); + }); + }); + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts index 42b2933defc..ab637f42eab 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/contentProviders/filePromptContentsProvider.test.ts @@ -9,10 +9,11 @@ import { VSBuffer } from '../../../../../../../base/common/buffer.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { randomInt } from '../../../../../../../base/common/numbers.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; -import { wait } from '../../../../../../../base/test/common/testUtils.js'; import { ReadableStream } from '../../../../../../../base/common/stream.js'; +import { NotPromptFile } from '../../../../common/promptFileReferenceErrors.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { randomBoolean, wait } from '../../../../../../../base/test/common/testUtils.js'; import { NullPolicyService } from '../../../../../../../platform/policy/common/policy.js'; import { Line } from '../../../../../../../editor/common/codecs/linesCodec/tokens/line.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; @@ -24,7 +25,7 @@ import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/ import { FilePromptContentProvider } from '../../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -suite('FilePromptContentsProvider', function () { +suite('FilePromptContentsProvider', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; @@ -48,7 +49,7 @@ suite('FilePromptContentsProvider', function () { instantiationService.stub(IConfigurationService, nullConfigService); }); - test('provides contents of a file', async function () { + test('• provides contents of a file', async () => { const fileService = instantiationService.get(IFileService); const fileName = `file-${randomInt(10000)}.prompt.md`; @@ -63,6 +64,7 @@ suite('FilePromptContentsProvider', function () { const contentsProvider = testDisposables.add(instantiationService.createInstance( FilePromptContentProvider, fileUri, + {}, )); let streamOrError: ReadableStream | Error | undefined; @@ -99,4 +101,144 @@ suite('FilePromptContentsProvider', function () { `Expected to receive '${expectedLine}', got '${receivedLine}'.`, ); }); + + suite('• options', () => { + suite('• allowNonPromptFiles', () => { + test('• true', async () => { + const fileService = instantiationService.get(IFileService); + + const fileName = (randomBoolean() === true) + ? `file-${randomInt(10_000)}.md` + : `file-${randomInt(10_000)}.txt`; + + const fileUri = URI.file(`/${fileName}`); + + if (await fileService.exists(fileUri)) { + await fileService.del(fileUri); + } + await fileService.writeFile(fileUri, VSBuffer.fromString('Hello, world!')); + await wait(5); + + const contentsProvider = testDisposables.add(instantiationService.createInstance( + FilePromptContentProvider, + fileUri, + { allowNonPromptFiles: true }, + )); + + let streamOrError: ReadableStream | Error | undefined; + testDisposables.add(contentsProvider.onContentChanged((event) => { + streamOrError = event; + })); + contentsProvider.start(); + + await wait(25); + + assertDefined( + streamOrError, + 'The `streamOrError` must be defined.', + ); + + assert( + !(streamOrError instanceof Error), + `Provider must produce a byte stream, got '${streamOrError}'.`, + ); + + const stream = new LinesDecoder(streamOrError); + + const receivedLines = await stream.consumeAll(); + assert.strictEqual( + receivedLines.length, + 1, + 'Must read the correct number of lines from the provider.', + ); + + const expectedLine = new Line(1, 'Hello, world!'); + const receivedLine = receivedLines[0]; + assert( + receivedLine.equals(expectedLine), + `Expected to receive '${expectedLine}', got '${receivedLine}'.`, + ); + }); + + test('• false', async () => { + const fileService = instantiationService.get(IFileService); + + const fileName = (randomBoolean() === true) + ? `file-${randomInt(10_000)}.md` + : `file-${randomInt(10_000)}.txt`; + + const fileUri = URI.file(`/${fileName}`); + + if (await fileService.exists(fileUri)) { + await fileService.del(fileUri); + } + await fileService.writeFile(fileUri, VSBuffer.fromString('Hello, world!')); + await wait(5); + + const contentsProvider = testDisposables.add(instantiationService.createInstance( + FilePromptContentProvider, + fileUri, + { allowNonPromptFiles: false }, + )); + + let streamOrError: ReadableStream | Error | undefined; + testDisposables.add(contentsProvider.onContentChanged((event) => { + streamOrError = event; + })); + contentsProvider.start(); + + await wait(25); + + assertDefined( + streamOrError, + 'The `streamOrError` must be defined.', + ); + + assert( + streamOrError instanceof NotPromptFile, + `Provider must produce an 'NotPromptFile' error, got '${streamOrError}'.`, + ); + }); + + test('• undefined', async () => { + const fileService = instantiationService.get(IFileService); + + const fileName = (randomBoolean() === true) + ? `file-${randomInt(10_000)}.md` + : `file-${randomInt(10_000)}.txt`; + + const fileUri = URI.file(`/${fileName}`); + + if (await fileService.exists(fileUri)) { + await fileService.del(fileUri); + } + await fileService.writeFile(fileUri, VSBuffer.fromString('Hello, world!')); + await wait(5); + + const contentsProvider = testDisposables.add(instantiationService.createInstance( + FilePromptContentProvider, + fileUri, + {}, + )); + + let streamOrError: ReadableStream | Error | undefined; + testDisposables.add(contentsProvider.onContentChanged((event) => { + streamOrError = event; + })); + contentsProvider.start(); + + await wait(25); + + assertDefined( + streamOrError, + 'The `streamOrError` must be defined.', + ); + + assert( + streamOrError instanceof NotPromptFile, + `Provider must produce an 'NotPromptFile' error, got '${streamOrError}'.`, + ); + }); + }); + }); }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 3f4767456c1..d5735551662 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { createURI } from '../testUtils/createUri.js'; +import { ChatMode } from '../../../../common/constants.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { ExpectedReference } from '../testUtils/expectedReference.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { OpenFailed } from '../../../../common/promptFileReferenceErrors.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; @@ -19,9 +21,10 @@ import { ILogService, NullLogService } from '../../../../../../../platform/log/c import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../../common/promptSyntax/constants.js'; import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { ExpectedDiagnosticError, ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { assertDefined } from '../../../../../../../base/common/types.js'; /** * Test helper to run unit tests for the {@link TextModelPromptParser} @@ -41,6 +44,7 @@ class TextModelPromptParserTest extends Disposable { constructor( uri: URI, initialContents: string[], + languageId: string = PROMPT_LANGUAGE_ID, @IFileService fileService: IFileService, @IInstantiationService initService: IInstantiationService, ) { @@ -57,7 +61,7 @@ class TextModelPromptParserTest extends Disposable { this.model = this._register( createTextModel( initialContents.join(lineEnding), - 'fooLang', + languageId, undefined, uri, ), @@ -65,10 +69,17 @@ class TextModelPromptParserTest extends Disposable { // create the parser instance this.parser = this._register( - initService.createInstance(TextModelPromptParser, this.model, []), + initService.createInstance(TextModelPromptParser, this.model, {}), ).start(); } + /** + * Wait for the prompt parsing/resolve process to finish. + */ + public allSettled(): Promise { + return this.parser.allSettled(); + } + /** * Validate the current state of the parser. */ @@ -95,6 +106,45 @@ class TextModelPromptParserTest extends Disposable { `[${this.model.uri}] Unexpected number of references.`, ); } + + /** + * Validate list of diagnostic objects of the prompt header. + */ + public async validateHeaderDiagnostics( + expectedDiagnostics: readonly TExpectedDiagnostic[], + ) { + await this.parser.allSettled(); + + const { header } = this.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + const { diagnostics } = header; + + for (let i = 0; i < expectedDiagnostics.length; i++) { + const diagnostic = diagnostics[i]; + + assertDefined( + diagnostic, + `Expected diagnostic #${i} be ${expectedDiagnostics[i]}, got 'undefined'.`, + ); + + try { + expectedDiagnostics[i].validateEqual(diagnostic); + } catch (_error) { + throw new Error( + `Expected diagnostic #${i} to be ${expectedDiagnostics[i]}, got '${diagnostic}'.`, + ); + } + } + + assert.strictEqual( + expectedDiagnostics.length, + diagnostics.length, + `Expected '${expectedDiagnostics.length}' diagnostic objects, got '${diagnostics.length}'.`, + ); + } } suite('TextModelPromptParser', () => { @@ -114,19 +164,21 @@ suite('TextModelPromptParser', () => { const createTest = ( uri: URI, initialContents: string[], + languageId: string = PROMPT_LANGUAGE_ID, ): TextModelPromptParserTest => { return disposables.add( instantiationService.createInstance( TextModelPromptParserTest, uri, initialContents, + languageId, ), ); }; - test('core logic #1', async () => { + test('• core logic #1', async () => { const test = createTest( - createURI('/foo/bar.md'), + URI.file('/foo/bar.md'), [ /* 01 */"The quick brown fox tries #file:/abs/path/to/file.md online yoga for the first time.", /* 02 */"Maria discovered a stray turtle roaming in her kitchen.", @@ -135,48 +187,48 @@ suite('TextModelPromptParser', () => { /* 05 */"Sometimes, the best code is the one you never have to write.", /* 06 */"A lone kangaroo once hopped into the local cafe, seeking free Wi-Fi.", /* 07 */"Critical #file:./folder/binary.file thinking is like coffee; best served strong [md link](/etc/hosts/random-file.txt) and without sugar.", - /* 08 */"Music is the mind’s way of doodling in the air.", + /* 08 */"Music is the mind's way of doodling in the air.", /* 09 */"Stargazing is just turning your eyes into cosmic explorers.", /* 10 */"Never trust a balloon salesman who hates birthdays.", /* 11 */"Running backward can be surprisingly enlightening.", - /* 12 */"There’s an art to whispering loudly.", + /* 12 */"There's an art to whispering loudly.", ], ); await test.validateReferences([ new ExpectedReference({ - uri: createURI('/abs/path/to/file.md'), + uri: URI.file('/abs/path/to/file.md'), text: '#file:/abs/path/to/file.md', path: '/abs/path/to/file.md', startLine: 1, startColumn: 27, pathStartColumn: 33, - childrenOrError: new OpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/abs/path/to/file.md'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/foo/folder/binary.file'), + uri: URI.file('/foo/folder/binary.file'), text: '#file:./folder/binary.file', path: './folder/binary.file', startLine: 7, startColumn: 10, pathStartColumn: 16, - childrenOrError: new OpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/foo/folder/binary.file'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/etc/hosts/random-file.txt'), + uri: URI.file('/etc/hosts/random-file.txt'), text: '[md link](/etc/hosts/random-file.txt)', path: '/etc/hosts/random-file.txt', startLine: 7, startColumn: 81, pathStartColumn: 91, - childrenOrError: new OpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/etc/hosts/random-file.txt'), 'File not found.'), }), ]); }); - test('core logic #2', async () => { + test('• core logic #2', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"The penguin wore sunglasses but never left the iceberg.", /* 02 */"I once saw a cloud that looked like an antique teapot.", @@ -198,47 +250,715 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[link text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', startLine: 3, startColumn: 43, pathStartColumn: 55, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/c/file_name.prompt.md'), + uri: URI.file('/absolute/c/file_name.prompt.md'), text: '[caption](../../../c/file_name.prompt.md)', path: '../../../c/file_name.prompt.md', startLine: 6, startColumn: 7, pathStartColumn: 17, - childrenOrError: new OpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/c/file_name.prompt.md'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/folder/main.rs'), + uri: URI.file('/absolute/folder/main.rs'), text: '#file:../../main.rs', path: '../../main.rs', startLine: 11, startColumn: 36, pathStartColumn: 42, - childrenOrError: new OpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/main.rs'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/samefile.jpeg'), + uri: URI.file('/absolute/folder/and/a/samefile.jpeg'), text: '#file:./somefolder/../samefile.jpeg', path: './somefolder/../samefile.jpeg', startLine: 11, startColumn: 56, pathStartColumn: 62, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), }), ]); }); - test('gets disposed with the model', async () => { + suite('• header', () => { + suite(' • metadata', () => { + test('• has correct \'prompt\' metadata', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: 'My prompt.'\t\t", + /* 03 */" something: true", /* unknown metadata record */ + /* 04 */" tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', 'tool_name2' ]\t\t", + /* 05 */" tools: [ 'tool_name3', \"tool_name4\" ]", /* duplicate `tools` record is ignored */ + /* 06 */" tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 07 */" mode: 'agent'", + /* 07 */" applyTo: 'frontend/**/*spec.ts'", + /* 08 */"---", + /* 09 */"The cactus on my desk has a thriving Instagram account.", + /* 10 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 12 */"Lunar rainbows only appear when you sing in falsetto.", + /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 11, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode, description, applyTo } = metadata; + assert.deepStrictEqual( + tools, + ['tool_name1', 'tool_name2'], + `Prompt header must have correct tools metadata.`, + ); + + assert.strictEqual( + mode, + 'agent', + `Prompt header must have correct 'mode' metadata.`, + ); + + assert.strictEqual( + description, + 'My prompt.', + `Prompt header must have correct 'description' metadata.`, + ); + + assert.strictEqual( + applyTo, + undefined, + `Prompt header must have no 'applyTo' metadata.`, + ); + }); + + test('• has correct \'instructions\' metadata', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.instructions.md'), + [ + /* 01 */"---", + /* 02 */"description: 'My prompt.'\t\t", + /* 03 */" something: true", /* unknown metadata record */ + /* 04 */" tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', 'tool_name2' ]\t\t", + /* 05 */" tools: [ 'tool_name3', \"tool_name4\" ]", /* duplicate `tools` record is ignored */ + /* 06 */" tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 07 */" mode: 'agent'", + /* 07 */" applyTo: 'frontend/**/*spec.ts'", + /* 08 */"---", + /* 09 */"The cactus on my desk has a thriving Instagram account.", + /* 10 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 12 */"Lunar rainbows only appear when you sing in falsetto.", + /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + INSTRUCTIONS_LANGUAGE_ID, + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 11, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode, description, applyTo } = metadata; + assert.deepStrictEqual( + tools, + ['tool_name1', 'tool_name2'], + `Prompt header must have correct tools metadata.`, + ); + + assert.strictEqual( + mode, + 'agent', + `Prompt header must have correct 'mode' metadata.`, + ); + + assert.strictEqual( + description, + 'My prompt.', + `Prompt header must have correct 'description' metadata.`, + ); + + assert.strictEqual( + applyTo, + 'frontend/**/*spec.ts', + `Prompt header must have no 'applyTo' metadata.`, + ); + }); + }); + + suite('• diagnostics', () => { + test('• core logic', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */" description: true \t ", + /* 03 */" mode: \"ask\"", + /* 04 */" something: true", /* unknown metadata record */ + /* 05 */"tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', ,'tool_name2' ] ", + /* 06 */" tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 07 */"tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 08 */"---", + /* 09 */"The cactus on my desk has a thriving Instagram account.", + /* 10 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 11 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 12 */"Lunar rainbows only appear when you sing in falsetto.", + /* 13 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 10, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 15, 2, 15 + 4), + 'Value of the \'description\' metadata must be \'string\', got \'boolean\'.', + ), + new ExpectedDiagnosticWarning( + new Range(4, 2, 4, 2 + 15), + 'Unknown metadata record \'something\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 38, 5, 38 + 12), + 'Duplicate tool name \'tool_name1\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 52, 5, 52 + 4), + 'Expected a tool name (string), got \'true\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 58, 5, 58 + 5), + 'Expected a tool name (string), got \'false\'.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 65, 5, 65 + 2), + 'Tool name cannot be empty.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 70, 5, 70 + 12), + 'Duplicate tool name \'tool_name2\'.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 2, 3, 2 + 11), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(6, 3, 6, 3 + 37), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(7, 1, 7, 1 + 19), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + ]); + }); + + suite('• applyTo metadata', () => { + suite('• language', () => { + test('• prompt', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/my.prompt.md'), + [ + /* 01 */"---", + /* 02 */"applyTo: '**/*'", + /* 03 */"mode: \"ask\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + PROMPT_LANGUAGE_ID, + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { applyTo, mode } = metadata; + assert.strictEqual( + mode, + ChatMode.Ask, + 'Mode metadata must have correct value.', + ); + + assert( + applyTo === undefined, + 'ApplyTo metadata must not be defined.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 1, 2, 1 + 15), + 'The \'applyTo\' metadata record is only valid in instruction files.', + ), + ]); + }); + + test('• instructions', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/my.prompt.md'), + [ + /* 01 */"---", + /* 02 */"applyTo: '**/*'", + /* 03 */"mode: \"edit\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + INSTRUCTIONS_LANGUAGE_ID, + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { applyTo, mode } = metadata; + assert.strictEqual( + mode, + ChatMode.Edit, + 'Mode metadata must have correct value.', + ); + + assert.strictEqual( + applyTo, + '**/*', + 'ApplyTo metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + }); + + test('• invalid glob pattern', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/my.prompt.md'), + [ + /* 01 */"---", + /* 02 */"mode: \"agent\"", + /* 03 */"applyTo: ''", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + INSTRUCTIONS_LANGUAGE_ID, + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { applyTo, mode } = metadata; + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + assert.strictEqual( + applyTo, + undefined, + 'ApplyTo metadata must not be defined.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(3, 10, 3, 10 + 2), + 'Invalid glob pattern \'\'.', + ), + ]); + }); + + suite('• tools and mode compatibility', () => { + suite('• tools is set', () => { + test('• ask mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"ask\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(3, 1, 3, 1 + 11), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + ]); + }); + + test('• edit mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"edit\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(3, 1, 3, 1 + 12), + 'Record \'mode\' is implied to have the \'agent\' value if \'tools\' record is present so the specified value will be ignored.', + ), + ]); + }); + + test('• agent mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"mode: \"agent\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + + test('• no mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + + suite('• tools is not set', () => { + test('• ask mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: ['my prompt', 'description.']", + /* 03 */"mode: \"ask\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Ask, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 14, 2, 14 + 29), + 'Value of the \'description\' metadata must be \'string\', got \'array\'.', + ), + ]); + }); + + test('• edit mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: my prompt description. \t\t \t\t ", + /* 03 */"mode: \"edit\"", + /* 04 */"---", + /* 05 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Edit, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticError( + new Range(2, 1, 2, 1 + 11), + 'Unexpected token \'description\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 12, 2, 12 + 2), + 'Unexpected token \': \'.', + ), + new ExpectedDiagnosticError( + new Range(2, 14, 2, 14 + 2), + 'Unexpected token \'my\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 17, 2, 17 + 6), + 'Unexpected token \'prompt\'.', + ), + new ExpectedDiagnosticError( + new Range(2, 24, 2, 24 + 12), + 'Unexpected token \'description.\'.', + ), + ]); + }); + + test('• agent mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"mode: \"agent\"", + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + + test('• no mode', async () => { + const test = createTest( + URI.file('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */"description: 'My prompt.'", + /* 03 */"---", + /* 04 */"The cactus on my desk has a thriving Instagram account.", + ], + ); + + await test.allSettled(); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools, mode } = metadata; + assert( + tools === undefined, + 'Tools metadata must not be defined.', + ); + + assert.strictEqual( + mode, + undefined, + 'Mode metadata must have correct value.', + ); + + await test.validateHeaderDiagnostics([]); + }); + }); + }); + }); + }); + + test('• gets disposed with the model', async () => { const test = createTest( - createURI('/some/path/file.prompt.md'), + URI.file('/some/path/file.prompt.md'), [ 'line1', 'line2', @@ -257,8 +977,8 @@ suite('TextModelPromptParser', () => { ); }); - test('toString() implementation', async () => { - const modelUri = createURI('/Users/legomushroom/repos/prompt-snippets/README.md'); + test('• toString() implementation', async () => { + const modelUri = URI.file('/Users/legomushroom/repos/prompt-snippets/README.md'); const test = createTest( modelUri, [ diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 1677ae1921c..f4fd4556e9a 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -4,27 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { ChatMode } from '../../../common/constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { extUri } from '../../../../../../base/common/resources.js'; -import { isWindows } from '../../../../../../base/common/platform.js'; import { Range } from '../../../../../../editor/common/core/range.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { IMockFolder, MockFilesystem } from './testUtils/mockFilesystem.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IPromptReference } from '../../../common/promptSyntax/parsers/types.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; import { FileService } from '../../../../../../platform/files/common/fileService.js'; import { NullPolicyService } from '../../../../../../platform/policy/common/policy.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; -import { TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; import { FileReference } from '../../../common/promptSyntax/codecs/tokens/fileReference.js'; import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; import { waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js'; +import { IPromptParserOptions, TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { INSTRUCTION_FILE_EXTENSION, PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { NotPromptFile, RecursiveReference, OpenFailed, FolderReference } from '../../../common/promptFileReferenceErrors.js'; @@ -40,10 +45,19 @@ class ExpectedReference { constructor( dirname: URI, - public readonly lineToken: FileReference, + public readonly linkToken: FileReference | MarkdownLink, public readonly errorCondition?: TErrorCondition, ) { - this.uri = extUri.resolvePath(dirname, lineToken.path); + this.uri = (linkToken.path.startsWith('/')) + ? URI.file(linkToken.path) + : URI.joinPath(dirname, linkToken.path); + } + + /** + * Range of the underlying file reference token. + */ + public get range(): Range { + return this.linkToken.range; } /** @@ -75,7 +89,9 @@ class TestPromptFileReference extends Disposable { /** * Run the test. */ - public async run() { + public async run( + options: Partial = {}, + ): Promise { // create the files structure on the disk await (this.initService.createInstance(MockFilesystem, this.fileStructure)).mock(); @@ -90,7 +106,7 @@ class TestPromptFileReference extends Disposable { this.initService.createInstance( FilePromptParser, this.rootFileUri, - [], + options, ), ).start(); @@ -104,6 +120,26 @@ class TestPromptFileReference extends Disposable { const expectedReference = this.expectedReferences[i]; const resolvedReference = resolvedReferences[i]; + if (expectedReference.linkToken instanceof MarkdownLink) { + assert( + resolvedReference?.subtype === 'markdown', + [ + `Expected ${i}th resolved reference to be a markdown link`, + `got '${resolvedReference}'.`, + ].join(', '), + ); + } + + if (expectedReference.linkToken instanceof FileReference) { + assert( + resolvedReference?.subtype === 'prompt', + [ + `Expected ${i}th resolved reference to be a #file: link`, + `got '${resolvedReference}'.`, + ].join(', '), + ); + } + assert( (resolvedReference) && (resolvedReference.uri.toString() === expectedReference.uri.toString()), @@ -113,6 +149,15 @@ class TestPromptFileReference extends Disposable { ].join(', '), ); + assert( + (resolvedReference) && + (resolvedReference.range.equalsRange(expectedReference.range)), + [ + `Expected ${i}th resolved reference range to be '${expectedReference.range}'`, + `got '${resolvedReference?.range}'.`, + ].join(', '), + ); + if (expectedReference.errorCondition === undefined) { assert( resolvedReference.errorCondition === undefined, @@ -139,8 +184,10 @@ class TestPromptFileReference extends Disposable { [ `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, - ].join('\n') + ].join('\n'), ); + + return rootReference; } } @@ -168,7 +215,7 @@ const createTestFileReference = ( return new FileReference(range, filePath); }; -suite('PromptFileReference (Unix)', function () { +suite('PromptFileReference', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; @@ -187,13 +234,23 @@ suite('PromptFileReference (Unix)', function () { instantiationService.stub(IFileService, nullFileService); instantiationService.stub(ILogService, nullLogService); instantiationService.stub(IConfigurationService, nullConfigService); + instantiationService.stub(IModelService, { getModel() { return null; } }); + instantiationService.stub(ILanguageService, { + guessLanguageIdByFilepathOrFirstLine(uri: URI) { + if (uri.path.endsWith(PROMPT_FILE_EXTENSION)) { + return PROMPT_LANGUAGE_ID; + } + + if (uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)) { + return INSTRUCTIONS_LANGUAGE_ID; + } + + return null; + } + }); }); test('• resolves nested file references', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -236,7 +293,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: `[](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, + contents: `[caption](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, }, { name: 'one_more_file_just_in_case.prompt.md', @@ -264,10 +321,9 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), - createTestFileReference( - `./some-other-folder/non-existing-folder`, - 2, - 1, + new MarkdownLink( + 2, 1, + '[]', '(./some-other-folder/non-existing-folder)', ), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), @@ -284,7 +340,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), - createTestFileReference('.', 1, 1), + new MarkdownLink( + 1, 1, + '[caption]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), new FolderReference( URI.joinPath(rootUri, './folder1/some-other-folder'), 'This folder is not a prompt file!', @@ -292,7 +351,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), - createTestFileReference('../file.txt', 2, 35), + new MarkdownLink( + 2, 34, + '[#file:file.txt]', '(../file.txt)', + ), new NotPromptFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), 'Ughh oh, that is not a prompt file!', @@ -300,14 +362,17 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( rootUri, - createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), + new MarkdownLink( + 3, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), - 'Failed to open non-existring prompt snippets file', + 'Failed to open non-existing prompt snippets file', ), ), new ExpectedReference( @@ -319,10 +384,13 @@ suite('PromptFileReference (Unix)', function () { ), ), new ExpectedReference( - URI.joinPath(rootUri, './some-other-folder/folder1'), - createTestFileReference('../../folder1', 5, 48), + URI.joinPath(rootUri, './folder1/some-other-folder'), + new MarkdownLink( + 5, 48, + '[]', '(../../folder1/)', + ), new FolderReference( - URI.joinPath(rootUri, './folder1'), + URI.joinPath(rootUri, './folder1/'), 'Uggh ohh!', ), ), @@ -333,10 +401,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• does not fall into infinite reference recursion', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'infinite-recursion'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -404,14 +468,13 @@ suite('PromptFileReference (Unix)', function () { [ new ExpectedReference( rootUri, - createTestFileReference('folder1/file3.prompt.md', 2, 9), + createTestFileReference('folder1/file3.prompt.md', 2, 14), ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), - createTestFileReference( - `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, - 3, - 23, + new MarkdownLink( + 3, 26, + '[another-file.prompt.md]', `(${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md)`, ), ), /** @@ -471,7 +534,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( rootUri, - createTestFileReference('./file1.md', 6, 2), + new MarkdownLink( + 6, 2, + '[some (snippet!) #name))]', '(./file1.md)', + ), new NotPromptFile( URI.joinPath(rootUri, './file1.md'), 'Uggh oh!', @@ -482,4 +548,1124 @@ suite('PromptFileReference (Unix)', function () { await test.run(); }); + + suite('• options', () => { + test('• allowNonPromptFiles', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: '## Some Header\nsome contents\n ', + }, + { + name: 'file2.md', + contents: '## Files\n\t- this file #file:folder1/file3.prompt.md \n\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!\n ', + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: `\n[](./some-other-folder/non-existing-folder)\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`, + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + }, + { + name: 'file.txt', + contents: 'contents of a non-prompt-snippet file', + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: `[](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 2, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + new MarkdownLink( + 2, 1, + '[]', '(./some-other-folder/non-existing-folder)', + ), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), + 'Reference to non-existing file cannot be opened.', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 3, + 26, + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + new MarkdownLink( + 1, 1, + '[]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), + new FolderReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + 'This folder is not a prompt file!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + new MarkdownLink( + 2, 34, + '[#file:file.txt]', '(../file.txt)', + ), + new NotPromptFile( + URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), + 'Ughh oh, that is not a prompt file!', + ), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 3, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), + 'Failed to open non-existing prompt snippets file', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/'), + createTestFileReference('./some-non-prompt-file.md', 5, 13), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), + 'Oh no!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './some-other-folder/folder1/'), + new MarkdownLink( + 5, 48, + '[]', '(../../folder1/)', + ), + new FolderReference( + URI.joinPath(rootUri, './folder1'), + 'Uggh ohh!', + ), + ), + ] + )); + + await test.run({ allowNonPromptFiles: true }); + }); + }); + + suite('• metadata', () => { + test('• tools', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\']', + 'mode: "agent" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: 'contents of a non-prompt-snippet file', + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 7, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + new MarkdownLink( + 5, 1, + '[]', '(./some-other-folder/non-existing-folder)', + ), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), + 'Reference to non-existing file cannot be opened.', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 6, 26, + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + new MarkdownLink( + 4, 1, + '[]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), + new FolderReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + 'This folder is not a prompt file!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + new MarkdownLink( + 5, 34, + '[#file:file.txt]', '(../file.txt)', + ), + new NotPromptFile( + URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), + 'Ughh oh, that is not a prompt file!', + ), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 8, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-existing/file.prompt.md', 6, 30), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), + 'Failed to open non-existing prompt snippets file', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-prompt-file.md', 10, 13), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), + 'Oh no!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './some-other-folder/folder1'), + new MarkdownLink( + 10, 48, + '[]', '(../../folder1/)', + ), + new FolderReference( + URI.joinPath(rootUri, './folder1'), + 'Uggh ohh!', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, description } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool1'], + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Root prompt description.', + 'Must have correct description metadata.', + ); + + assertDefined( + allToolsMetadata, + 'All tools metadata must to be defined.', + ); + assert.deepStrictEqual( + allToolsMetadata, + ['my-tool1', 'my-tool3', 'my-tool2'], + 'Must have correct all tools metadata.', + ); + }); + + suite('• applyTo', () => { + test('• prompt language', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'applyTo: \'**/*\'', + 'tools: [ false, \'my-tool12\' , ]', + 'description: \'Description of my prompt.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 7, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 8, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description, applyTo } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool12'], + 'Must have correct \'tools\' metadata.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Must have correct \'mode\' metadata.', + ); + + assert.strictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool12', + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + + assert.strictEqual( + applyTo, + undefined, + 'Must have no \'applyTo\' metadata.', + ); + }); + + + test('• instructions language', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.instructions.md', + contents: [ + '---', + 'applyTo: \'**/*\'', + 'tools: [ false, \'my-tool12\' , ]', + 'description: \'Description of my prompt.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.instructions.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 7, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 8, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description, applyTo } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool12'], + 'Must have correct \'tools\' metadata.', + ); + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Must have correct \'mode\' metadata.', + ); + + assert.strictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool12', + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + + assert.strictEqual( + applyTo, + '**/*', + 'Must have no \'applyTo\' metadata.', + ); + }); + }); + + suite('• tools and mode compatibility', () => { + test('• tools are ignored if root prompt in the ask mode', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode: "ask" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + 'mode: \'agent\'\t', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct \'tools\' metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Ask, + 'Must have correct \'mode\' metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + null, + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are ignored if root prompt in the edit mode', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode:\t\t"edit"\t\t', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct \'tools\' metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Edit, + 'Must have correct tools metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + null, + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are not ignored if root prompt in the agent mode', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Description of my prompt.\'', + 'mode: \t\t "agent" \t\t ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + undefined, + 'Must have correct \'tools\' metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Agent, + 'Must have correct \'mode\' metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + }); + + test('• tools are not ignored if root prompt implicitly in the agent mode', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool12\' , ]', + 'description: \'Description of my prompt.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , \'my-tool3\' , ]', + 'something: true', + 'mode: \'agent\'\t', + '---', + '', + '', + 'and some more content', + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, mode, description } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool12'], + 'Must have correct \'tools\' metadata.', + ); + + assert.deepStrictEqual( + mode, + ChatMode.Agent, + 'Must have correct \'mode\' metadata.', + ); + + assert.deepStrictEqual( + description, + 'Description of my prompt.', + 'Must have correct \'description\' metadata.', + ); + + assert.deepStrictEqual( + allToolsMetadata, + [ + 'my-tool12', + 'my-tool1', + 'my-tool2', + 'my-tool3', + ], + 'Must have correct all tools metadata.', + ); + }); + }); + }); }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index bced4a5e45e..ad4d983b6da 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -4,23 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { createURI } from '../testUtils/createUri.js'; +import * as sinon from 'sinon'; +import { ChatMode } from '../../../../common/constants.js'; import { URI } from '../../../../../../../base/common/uri.js'; +import { MockFilesystem } from '../testUtils/mockFilesystem.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; -import { waitRandom } from '../../../../../../../base/test/common/testUtils.js'; import { IPromptsService } from '../../../../common/promptSyntax/service/types.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { IPromptFileReference } from '../../../../common/promptSyntax/parsers/types.js'; import { FileService } from '../../../../../../../platform/files/common/fileService.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; -import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { randomBoolean, waitRandom } from '../../../../../../../base/test/common/testUtils.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../../common/promptSyntax/constants.js'; +import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { INSTRUCTION_FILE_EXTENSION, PROMPT_FILE_EXTENSION } from '../../../../../../../platform/prompts/common/constants.js'; import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { pick } from '../../../../../../../base/common/arrays.js'; /** * Helper class to assert the properties of a link. @@ -93,20 +102,42 @@ suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let service: IPromptsService; - let instantiationService: TestInstantiationService; + let instaService: TestInstantiationService; setup(async () => { - instantiationService = disposables.add(new TestInstantiationService()); - instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IFileService, disposables.add(instantiationService.createInstance(FileService))); + instaService = disposables.add(new TestInstantiationService()); + instaService.stub(ILogService, new NullLogService()); + instaService.stub(IConfigurationService, new TestConfigurationService()); + + const fileService = disposables.add(instaService.createInstance(FileService)); + instaService.stub(IFileService, fileService); + instaService.stub(IModelService, { getModel() { return null; } }); + instaService.stub(ILanguageService, { + guessLanguageIdByFilepathOrFirstLine(uri: URI) { + if (uri.path.endsWith(PROMPT_FILE_EXTENSION)) { + return PROMPT_LANGUAGE_ID; + } + + if (uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)) { + return INSTRUCTIONS_LANGUAGE_ID; + } + + return 'plaintext'; + } + }); - service = disposables.add(instantiationService.createInstance(PromptsService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); + + service = disposables.add(instaService.createInstance(PromptsService)); }); suite('• getParserFor', () => { test('• provides cached parser instance', async () => { - const langId = 'fooLang'; + // both languages must yield the same result + const languageId = (randomBoolean()) + ? PROMPT_LANGUAGE_ID + : INSTRUCTIONS_LANGUAGE_ID; /** * Create a text model, get a parser for it, and perform basic assertions. @@ -114,9 +145,9 @@ suite('PromptsService', () => { const model1 = disposables.add(createTextModel( 'test1\n\t#file:./file.md\n\n\n [bin file](/root/tmp.bin)\t\n', - langId, + languageId, undefined, - createURI('/Users/vscode/repos/test/file1.txt'), + URI.file('/Users/vscode/repos/test/file1.txt'), )); const parser1 = service.getSyntaxParserFor(model1); @@ -145,12 +176,12 @@ suite('PromptsService', () => { parser1.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -185,9 +216,9 @@ suite('PromptsService', () => { const model2 = disposables.add(createTextModel( 'some text #file:/absolute/path.txt \t\ntest-text2', - langId, + languageId, undefined, - createURI('/Users/vscode/repos/test/some-folder/file.md'), + URI.file('/Users/vscode/repos/test/some-folder/file.md'), )); // wait for some random amount of time @@ -242,7 +273,7 @@ suite('PromptsService', () => { parser2.allReferences, [ new ExpectedLink( - createURI('/absolute/path.txt'), + URI.file('/absolute/path.txt'), new Range(1, 11, 1, 11 + 24), new Range(1, 17, 1, 17 + 18), ), @@ -261,12 +292,12 @@ suite('PromptsService', () => { parser1_1.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -333,12 +364,12 @@ suite('PromptsService', () => { parser1_2.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -378,9 +409,9 @@ suite('PromptsService', () => { // we cannot use the same model since it was already disposed const model2_1 = disposables.add(createTextModel( 'some text #file:/absolute/path.txt \n [caption](.copilot/prompts/test.prompt.md)\t\n\t\n more text', - langId, + languageId, undefined, - createURI('/Users/vscode/repos/test/some-folder/file.md'), + URI.file('/Users/vscode/repos/test/some-folder/file.md'), )); const parser2_1 = service.getSyntaxParserFor(model2_1); @@ -413,13 +444,13 @@ suite('PromptsService', () => { [ // the first link didn't change new ExpectedLink( - createURI('/absolute/path.txt'), + URI.file('/absolute/path.txt'), new Range(1, 11, 1, 11 + 24), new Range(1, 17, 1, 17 + 18), ), // the second link is new new ExpectedLink( - createURI('/Users/vscode/repos/test/some-folder/.copilot/prompts/test.prompt.md'), + URI.file('/Users/vscode/repos/test/some-folder/.copilot/prompts/test.prompt.md'), new Range(2, 2, 2, 2 + 42), new Range(2, 12, 2, 12 + 31), ), @@ -434,7 +465,7 @@ suite('PromptsService', () => { ' \t #file:../file.md\ntest1\n\t\n [another file](/Users/root/tmp/file2.txt)\t\n', langId, undefined, - createURI('/repos/test/file1.txt'), + URI.file('/repos/test/file1.txt'), )); const parser = service.getSyntaxParserFor(model); @@ -455,12 +486,12 @@ suite('PromptsService', () => { parser.allReferences, [ new ExpectedLink( - createURI('/repos/file.md'), + URI.file('/repos/file.md'), new Range(1, 4, 1, 4 + 16), new Range(1, 10, 1, 10 + 10), ), new ExpectedLink( - createURI('/Users/root/tmp/file2.txt'), + URI.file('/Users/root/tmp/file2.txt'), new Range(4, 3, 4, 3 + 41), new Range(4, 18, 4, 18 + 25), ), @@ -481,13 +512,13 @@ suite('PromptsService', () => { [ // link1 didn't change new ExpectedLink( - createURI('/repos/file.md'), + URI.file('/repos/file.md'), new Range(1, 4, 1, 4 + 16), new Range(1, 10, 1, 10 + 10), ), // link2 changed in the file name only new ExpectedLink( - createURI('/Users/root/tmp/file3.txt'), + URI.file('/Users/root/tmp/file3.txt'), new Range(4, 3, 4, 3 + 41), new Range(4, 18, 4, 18 + 25), ), @@ -495,7 +526,7 @@ suite('PromptsService', () => { ); }); - test('• throws if disposed model provided', async function () { + test('• throws if a disposed model provided', async function () { const model = disposables.add(createTextModel( 'test1\ntest2\n\ntest3\t\n', 'barLang', @@ -511,4 +542,1657 @@ suite('PromptsService', () => { }, 'Cannot create a prompt parser for a disposed model.'); }); }); + + suite('• getCombinedToolsMetadata', () => { + suite('• agent mode', () => { + test('• explicit', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\']', + 'mode: "agent" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.deepStrictEqual( + tools, + [ + 'my-tool1', + 'my-tool3', + 'my-tool2', + ], + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\']', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.deepStrictEqual( + tools, + [ + 'my-tool1', + 'my-tool3', + 'my-tool2', + ], + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit (incorrect value)', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + // both modes must yield the same result + const incorrectMode = (randomBoolean()) + ? ChatMode.Ask + : ChatMode.Edit; + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\']', + `mode: '${incorrectMode}'`, + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Agent, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.deepStrictEqual( + tools, + [ + 'my-tool1', + 'my-tool3', + 'my-tool2', + ], + 'Combined metadata \'tools\' must have correct value.', + ); + }); + }); + + suite('• edit mode', () => { + test('• explicit', async () => { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'mode: "edit"', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'mode: \'ask\'\t', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Edit, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'mode: \'ask\'', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'description: "My prompt."', + 'mode: "edit"\t\t', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Edit, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit (incorrect value)', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + // both modes must yield the same result + const incorrectMode = (randomBoolean()) + ? 'unknown-mode-1' + : 'unknown-mode-2'; + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + `mode: '${incorrectMode}'`, + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'mode: \'ask\'', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + 'mode: \'edit\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Edit, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + }); + + suite('• ask mode', () => { + test('• explicit', async () => { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'mode:\t\t"ask"\t', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'description: "some text"', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Ask, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'description: "Another prompt description."', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'description: "My prompt."', + 'mode: "ask"\t\t', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Ask, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + + test('• implicit (incorrect value)', async function () { + const rootFolderName = 'gets-combined-tools-metadata'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + const rootFileUri = URI.file(`${rootFolder}/${rootFileName}`); + + // both modes must yield the same result + const incorrectMode = (randomBoolean()) + ? 'unknown-mode-1' + : 'unknown-mode-2'; + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + `mode: '${incorrectMode}'`, + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'something: true', + 'mode: \'ask\'\t', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getCombinedToolsMetadata([rootFileUri]); + + assertDefined( + metadata, + 'Combined metadata must be defined.', + ); + + const { tools, mode } = metadata; + + assert.strictEqual( + mode, + ChatMode.Ask, + 'Combined metadata \'mode\' must have correct value.', + ); + + assert.strictEqual( + tools, + undefined, + 'Combined metadata \'tools\' must have correct value.', + ); + }); + }); + }); + + suite('• getAllMetadata', () => { + test('• explicit', async function () { + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + + const rootFileName = 'file2.prompt.md'; + + const rootFolderUri = URI.file(rootFolder); + const rootFileUri = URI.joinPath(rootFolderUri, rootFileName); + + await (instaService.createInstance(MockFilesystem, + // the file structure to be created on the disk for the test + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: rootFileName, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\', , true]', + 'mode: "agent" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + 'mode: \'edit\'', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.instructions.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'mode: \'ask\'\t', + 'description: "File 4 splendid description."', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.instructions.md', + contents: [ + '---', + 'description: "Another file description."', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + 'applyTo: "**/*.tsx"', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.instructions.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }])).mock(); + + const metadata = await service + .getAllMetadata([rootFileUri]); + + assert.deepStrictEqual( + metadata, + [{ + uri: rootFileUri, + metadata: { + description: 'Root prompt description.', + tools: ['my-tool1'], + mode: 'agent', + applyTo: undefined, + }, + children: [ + { + uri: URI.joinPath(rootFolderUri, 'folder1/file3.prompt.md'), + metadata: { + description: undefined, + applyTo: undefined, + tools: ['my-tool1'], + mode: 'agent', + }, + children: [ + { + uri: URI.joinPath(rootFolderUri, 'folder1/some-other-folder/yetAnotherFolder🤭/another-file.instructions.md'), + metadata: { + description: 'Another file description.', + tools: ['my-tool3', 'my-tool2'], + mode: 'agent', + applyTo: '**/*.tsx', + }, + children: undefined, + }, + ], + }, + { + uri: URI.joinPath(rootFolderUri, 'folder1/some-other-folder/file4.prompt.md'), + metadata: { + tools: ['my-tool1', 'my-tool2'], + description: 'File 4 splendid description.', + applyTo: undefined, + mode: 'agent', + }, + children: undefined, + } + ], + }], + ); + }); + }); + + suite('• findInstructionFilesFor', () => { + teardown(() => { + sinon.restore(); + }); + + test('• finds correct instruction files', async () => { + const rootFolderName = 'finds-instruction-files'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + const userPromptsFolderName = '/tmp/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolderName); + + sinon.stub(service, 'listPromptFiles') + .returns(Promise.resolve([ + // local instructions + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file3.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file4.instructions.md'), + storage: 'local', + type: 'instructions', + }, + // user instructions + { + uri: URI.joinPath(userPromptsFolderUri, 'file10.instructions.md'), + storage: 'user', + type: 'instructions', + }, + { + uri: URI.joinPath(userPromptsFolderUri, 'file11.instructions.md'), + storage: 'user', + type: 'instructions', + }, + ])); + + // mock current workspace file structure + await (instaService.createInstance(MockFilesystem, + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: '.github/prompts', + children: [ + { + name: 'file1.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 1.\'', + 'applyTo: "**/*.tsx"', + '---', + 'Some instructions 1 contents.', + ], + }, + { + name: 'file2.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 2.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 2 contents.', + ], + }, + { + name: 'file3.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 3.\'', + 'applyTo: "**/folder2/*.tsx"', + '---', + 'Some instructions 3 contents.', + ], + }, + { + name: 'file4.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 4.\'', + 'applyTo: "src/build/*.tsx"', + '---', + 'Some instructions 4 contents.', + ], + }, + { + name: 'file5.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 5.\'', + '---', + 'Some prompt 5 contents.', + ], + }, + ], + }, + { + name: 'folder1', + children: [ + { + name: 'main.tsx', + contents: 'console.log("Haalou!")', + }, + ], + }, + ], + }])).mock(); + + // mock user data instructions + await (instaService.createInstance(MockFilesystem, [ + { + name: userPromptsFolderName, + children: [ + { + name: 'file10.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 10.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 10 contents.', + ], + }, + { + name: 'file11.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 11.\'', + 'applyTo: "**/folder1/*.py"', + '---', + 'Some instructions 11 contents.', + ], + }, + { + name: 'file12.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 12.\'', + '---', + 'Some prompt 12 contents.', + ], + }, + ], + } + ])).mock(); + + const instructions = await service + .findInstructionFilesFor([ + URI.joinPath(rootFolderUri, 'folder1/main.tsx'), + ]); + + assert.deepStrictEqual( + instructions.map(pick('path')), + [ + // local instructions + URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path, + URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md').path, + // user instructions + URI.joinPath(userPromptsFolderUri, 'file10.instructions.md').path, + ], + 'Must find correct instruction files.', + ); + }); + + test('• does not have duplicates', async () => { + const rootFolderName = 'finds-instruction-files-without-duplicates'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + const userPromptsFolderName = '/tmp/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolderName); + + sinon.stub(service, 'listPromptFiles') + .returns(Promise.resolve([ + // local instructions + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file3.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file4.instructions.md'), + storage: 'local', + type: 'instructions', + }, + // user instructions + { + uri: URI.joinPath(userPromptsFolderUri, 'file10.instructions.md'), + storage: 'user', + type: 'instructions', + }, + { + uri: URI.joinPath(userPromptsFolderUri, 'file11.instructions.md'), + storage: 'user', + type: 'instructions', + }, + ])); + + // mock current workspace file structure + await (instaService.createInstance(MockFilesystem, + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: '.github/prompts', + children: [ + { + name: 'file1.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 1.\'', + 'applyTo: "**/*.tsx"', + '---', + 'Some instructions 1 contents.', + ], + }, + { + name: 'file2.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 2.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 2 contents. [](./file1.instructions.md)', + ], + }, + { + name: 'file3.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 3.\'', + 'applyTo: "**/folder2/*.tsx"', + '---', + 'Some instructions 3 contents.', + ], + }, + { + name: 'file4.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 4.\'', + 'applyTo: "src/build/*.tsx"', + '---', + '[](./file3.instructions.md) Some instructions 4 contents.', + ], + }, + { + name: 'file5.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 5.\'', + '---', + 'Some prompt 5 contents.', + ], + }, + ], + }, + { + name: 'folder1', + children: [ + { + name: 'main.tsx', + contents: 'console.log("Haalou!")', + }, + ], + }, + ], + }])).mock(); + + // mock user data instructions + await (instaService.createInstance(MockFilesystem, [ + { + name: userPromptsFolderName, + children: [ + { + name: 'file10.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 10.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 10 contents.', + ], + }, + { + name: 'file11.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 11.\'', + 'applyTo: "**/folder1/*.py"', + '---', + 'Some instructions 11 contents.', + ], + }, + { + name: 'file12.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 12.\'', + '---', + 'Some prompt 12 contents.', + ], + }, + ], + } + ])).mock(); + + const instructions = await service + .findInstructionFilesFor([ + URI.joinPath(rootFolderUri, 'folder1/main.tsx'), + URI.joinPath(rootFolderUri, 'folder1/index.tsx'), + URI.joinPath(rootFolderUri, 'folder1/constants.tsx'), + ]); + + assert.deepStrictEqual( + instructions.map(pick('path')), + [ + // local instructions + URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md').path, + URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md').path, + // user instructions + URI.joinPath(userPromptsFolderUri, 'file10.instructions.md').path, + ], + 'Must find correct instruction files.', + ); + }); + }); }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts index ea07bd5d33f..2046a69975b 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts @@ -11,7 +11,9 @@ import { isWindows } from '../../../../../../../base/common/platform.js'; * On `Windows`, absolute paths are prefixed with the disk name. */ export const createURI = (linkPath: string): URI => { - return URI.file(createPath(linkPath)); + return URI.file( + createPath(linkPath), + ); }; /** diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts new file mode 100644 index 00000000000..5c39b6059dd --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { assertNever } from '../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning, TDiagnostic } from '../../../../common/promptSyntax/parsers/promptHeader/diagnostics.js'; + +/** + * Base class for all expected diagnostics used in the unit tests. + */ +abstract class ExpectedDiagnostic extends PromptMetadataDiagnostic { + /** + * Validate that the provided diagnostic is equal to this object. + */ + public validateEqual(other: TDiagnostic) { + this.validateTypesEqual(other); + + assert.strictEqual( + this.message, + other.message, + `Expected message '${this.message}', got '${other.message}'.`, + ); + + assert( + this.range + .equalsRange(other.range), + `Expected range '${this.range}', got '${other.range}'.`, + ); + } + + /** + * Validate that the provided diagnostic is of the same + * diagnostic type as this object. + */ + private validateTypesEqual(other: TDiagnostic) { + if (other instanceof PromptMetadataWarning) { + assert( + this instanceof ExpectedDiagnosticWarning, + `Expected a warning diagnostic object, got '${other}'.`, + ); + + return; + } + + if (other instanceof PromptMetadataError) { + assert( + this instanceof ExpectedDiagnosticError, + `Expected a error diagnostic object, got '${other}'.`, + ); + + return; + } + + assertNever( + other, + `Unknown diagnostic type '${other}'.`, + ); + } +} + +/** + * Expected warning diagnostic object for testing purposes. + */ +export class ExpectedDiagnosticWarning extends ExpectedDiagnostic { + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `expected-diagnostic/warning(${this.message})${this.range}`; + } +} + +/** + * Expected error diagnostic object for testing purposes. + */ +export class ExpectedDiagnosticError extends ExpectedDiagnostic { + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `expected-diagnostic/error(${this.message})${this.range}`; + } +} + +/** + * Type for any expected diagnostic object. + */ +export type TExpectedDiagnostic = ExpectedDiagnosticWarning | ExpectedDiagnosticError; diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts index 80db5124e43..09d00b4c1dc 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts @@ -6,6 +6,7 @@ import { URI } from '../../../../../../../base/common/uri.js'; import { assert } from '../../../../../../../base/common/assert.js'; import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { wait } from '../../../../../../../base/test/common/testUtils.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; /** @@ -19,7 +20,7 @@ interface IMockFilesystemNode { * Represents a `file` node. */ export interface IMockFile extends IMockFilesystemNode { - contents: string; + contents: string | readonly string[]; } /** @@ -47,12 +48,19 @@ export class MockFilesystem { * Starts the mock process. */ public async mock(): Promise[]> { - return await Promise.all( + const result = await Promise.all( this.folders .map((folder) => { return this.mockFolder(folder); }), ); + + // wait for the filesystem event to settle before proceeding + // this is temporary workaround and should be fixed once we + // improve behavior of the `settled()` / `allSettled()` methods + await wait(25); + + return result; } /** @@ -90,7 +98,11 @@ export class MockFilesystem { `File '${folderUri.path}' already exists.`, ); - await this.fileService.writeFile(childUri, VSBuffer.fromString(child.contents)); + const contents: string = (typeof child.contents === 'string') + ? child.contents + : child.contents.join('\n'); + + await this.fileService.writeFile(childUri, VSBuffer.fromString(contents)); resolvedChildren.push({ ...child, diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 4342e42b78a..670cf234a7b 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -33,7 +33,7 @@ const mockConfigService = (value: T): IConfigurationService => { ); assert( - [PromptsConfig.KEY, PromptsConfig.LOCATIONS_KEY].includes(key), + [PromptsConfig.KEY, PromptsConfig.PROMPT_LOCATIONS_KEY].includes(key), `Unsupported configuration key '${key}'.`, ); @@ -109,7 +109,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator(undefined, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -123,7 +123,7 @@ suite('PromptFilesLocator', () => { }, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - await locator.listFiles(), + await locator.listFiles('prompt'), [], 'No prompts must be found.', ); @@ -136,7 +136,7 @@ suite('PromptFilesLocator', () => { ], EMPTY_WORKSPACE, []); assert.deepStrictEqual( - await locator.listFiles(), + await locator.listFiles('prompt'), [], 'No prompts must be found.', ); @@ -146,7 +146,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator(null, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -157,7 +157,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator('/etc/hosts/prompts', EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -210,7 +210,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -287,7 +287,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -447,7 +447,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -531,7 +531,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -691,7 +691,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -771,7 +771,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -931,7 +931,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -1016,7 +1016,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/my.prompt.md').fsPath, @@ -1103,7 +1103,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -1224,7 +1224,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md').path, @@ -1345,7 +1345,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md').fsPath, @@ -1469,7 +1469,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -1592,7 +1592,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ // all of these are due to the `.github/prompts` setting @@ -1707,7 +1707,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -1913,7 +1913,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -2038,7 +2038,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -2274,7 +2274,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -2390,7 +2390,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - locator.getConfigBasedSourceFolders() + locator.getConfigBasedSourceFolders('prompt') .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts').fsPath, diff --git a/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/treeUrils.test.ts b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/treeUrils.test.ts new file mode 100644 index 00000000000..d1d3dfa7c82 --- /dev/null +++ b/code/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/treeUrils.test.ts @@ -0,0 +1,482 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { randomInt } from '../../../../../../../base/common/numbers.js'; +import { curry, flatten, forEach, map } from '../../../../common/promptSyntax/utils/treeUtils.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; + +suite('tree utilities', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('• flatten', () => { + const tree = { + id: '1', + children: [ + { + id: '1.1', + }, + { + id: '1.2', + children: [ + { + id: '1.2.1', + children: [ + { + id: '1.2.1.1', + }, + { + id: '1.2.1.2', + }, + { + id: '1.2.1.3', + } + ], + }, + { + id: '1.2.2', + }, + ] + }, + ], + }; + + assert.deepStrictEqual(flatten(tree), [ + tree, + tree.children[0], + tree.children[1], + tree.children[1].children![0], + tree.children[1].children![0].children![0], + tree.children[1].children![0].children![1], + tree.children[1].children![0].children![2], + tree.children[1].children![1], + ]); + + assert.deepStrictEqual(flatten({}), [{}]); + }); + + suite('• forEach', () => { + test('• iterates though all nodes', () => { + const tree = { + id: '1', + children: [ + { + id: '1.1', + }, + { + id: '1.2', + children: [ + { + id: '1.2.1', + children: [ + { + id: '1.2.1.1', + }, + { + id: '1.2.1.2', + }, + { + id: '1.2.1.3', + } + ], + }, + { + id: '1.2.2', + }, + ] + }, + ], + }; + + const treeCopy = JSON.parse(JSON.stringify(tree)); + + const seenIds: string[] = []; + forEach((node) => { + seenIds.push(node.id); + return false; + }, tree); + + assert.deepStrictEqual(seenIds, [ + '1', + '1.1', + '1.2', + '1.2.1', + '1.2.1.1', + '1.2.1.2', + '1.2.1.3', + '1.2.2', + ]); + + assert.deepStrictEqual( + treeCopy, + tree, + 'forEach should not modify the tree', + ); + }); + + test('• can be stopped prematurely', () => { + const tree = { + id: '1', + children: [ + { + id: '1.1', + }, + { + id: '1.2', + children: [ + { + id: '1.2.1', + children: [ + { + id: '1.2.1.1', + }, + { + id: '1.2.1.2', + }, + { + id: '1.2.1.3', + children: [ + { + id: '1.2.1.3.1', + }, + ], + } + ], + }, + { + id: '1.2.2', + }, + ] + }, + ], + }; + + const treeCopy = JSON.parse(JSON.stringify(tree)); + + const seenIds: string[] = []; + forEach((node) => { + seenIds.push(node.id); + + if (node.id === '1.2.1') { + return true; // stop traversing + } + + return false; + }, tree); + + assert.deepStrictEqual(seenIds, [ + '1', + '1.1', + '1.2', + '1.2.1', + ]); + + assert.deepStrictEqual( + treeCopy, + tree, + 'forEach should not modify the tree', + ); + }); + }); + + suite('• map', () => { + test('• maps a tree', () => { + interface ITree { + id: string; + children?: ITree[]; + } + + const tree: ITree = { + id: '1', + children: [ + { + id: '1.1', + }, + { + id: '1.2', + children: [ + { + id: '1.2.1', + children: [ + { + id: '1.2.1.1', + }, + { + id: '1.2.1.2', + }, + { + id: '1.2.1.3', + } + ], + }, + { + id: '1.2.2', + }, + ] + }, + ], + }; + + const treeCopy = JSON.parse(JSON.stringify(tree)); + + const newRootNode = { + newId: '__1__', + }; + + const newChildNode = { + newId: '__1.2.1.3__', + }; + + const newTree = map((node) => { + if (node.id === '1') { + return newRootNode; + } + + if (node.id === '1.2.1.3') { + return newChildNode; + } + + return { + newId: `__${node.id}__`, + }; + }, tree); + + assert.deepStrictEqual(newTree, { + newId: '__1__', + children: [ + { + newId: '__1.1__', + }, + { + newId: '__1.2__', + children: [ + { + newId: '__1.2.1__', + children: [ + { + newId: '__1.2.1.1__', + }, + { + newId: '__1.2.1.2__', + }, + { + newId: '__1.2.1.3__', + }, + ], + }, + { + newId: '__1.2.2__', + }, + ] + }, + ], + }); + + assert( + newRootNode === newTree, + 'Map should not replace return node reference (root node).', + ); + + assert( + newChildNode === newTree.children![1].children![0].children![2], + 'Map should not replace return node reference (child node).', + ); + + assert.deepStrictEqual( + treeCopy, + tree, + 'forEach should not modify the tree', + ); + }); + + test('• callback can control resulting children', () => { + interface ITree { + id: string; + children?: ITree[]; + } + + const tree: ITree = { + id: '1', + children: [ + { id: '1.1' }, + { + id: '1.2', + children: [ + { + id: '1.2.1', + children: [ + { id: '1.2.1.1' }, + { id: '1.2.1.2' }, + { + id: '1.2.1.3', + children: [ + { + id: '1.2.1.3.1', + }, + { + id: '1.2.1.3.2', + }, + ], + } + ], + }, + { + id: '1.2.2', + children: [ + { id: '1.2.2.1' }, + { id: '1.2.2.2' }, + { id: '1.2.2.3' }, + ], + }, + { + id: '1.2.3', + children: [ + { id: '1.2.3.1' }, + { id: '1.2.3.2' }, + { id: '1.2.3.3' }, + { id: '1.2.3.4' }, + ], + }, + ] + }, + ], + }; + + const treeCopy = JSON.parse(JSON.stringify(tree)); + + const newNodeWithoutChildren = { + newId: '__1.2.1.3__', + children: undefined, + }; + + const newTree = map((node, newChildren) => { + // validates that explicitly setting `children` to + // `undefined` will be preserved on the resulting new node + if (node.id === '1.2.1.3') { + return newNodeWithoutChildren; + } + + // validates that setting `children` to a new array + // will be preserved on the resulting new node + if (node.id === '1.2.2') { + assert.deepStrictEqual( + newChildren, + [ + { newId: '__1.2.2.1__' }, + { newId: '__1.2.2.2__' }, + { newId: '__1.2.2.3__' }, + ], + `Node '${node.id}' must have correct new children.`, + ); + + return { + newId: `__${node.id}__`, + children: [newChildren[2]], + }; + } + + // validates that modifying `newChildren` directly + // will be preserved on the resulting new node + if (node.id === '1.2.3') { + assert.deepStrictEqual( + newChildren, + [ + { newId: '__1.2.3.1__' }, + { newId: '__1.2.3.2__' }, + { newId: '__1.2.3.3__' }, + { newId: '__1.2.3.4__' }, + ], + `Node '${node.id}' must have correct new children.`, + ); + + newChildren.length = 2; + + return { + newId: `__${node.id}__`, + }; + } + + // convert to a new node in all other cases + return { + newId: `__${node.id}__`, + }; + }, tree); + + assert.deepStrictEqual(newTree, { + newId: '__1__', + children: [ + { newId: '__1.1__' }, + { + newId: '__1.2__', + children: [ + { + newId: '__1.2.1__', + children: [ + { newId: '__1.2.1.1__' }, + { newId: '__1.2.1.2__' }, + { + newId: '__1.2.1.3__', + children: undefined, + }, + ], + }, + { + newId: '__1.2.2__', + children: [ + { newId: '__1.2.2.3__' }, + ], + }, + { + newId: '__1.2.3__', + children: [ + { newId: '__1.2.3.1__' }, + { newId: '__1.2.3.2__' }, + ], + }, + ] + }, + ], + }); + + assert( + newNodeWithoutChildren === newTree.children![1].children![0].children![2], + 'Map should not replace return node reference (node without children).', + ); + + assert.deepStrictEqual( + treeCopy, + tree, + 'forEach should not modify the tree', + ); + }); + }); + + test('• curry', () => { + const originalFunction = (a: number, b: number, c: number) => { + return a + b + c; + }; + + const firstArgument = randomInt(100, -100); + const curriedFunction = curry(originalFunction, firstArgument); + + let iterations = 10; + while (iterations-- > 0) { + const secondArgument = randomInt(100, -100); + const thirdArgument = randomInt(100, -100); + + assert.strictEqual( + curriedFunction(secondArgument, thirdArgument), + originalFunction(firstArgument, secondArgument, thirdArgument), + 'Curried and original functions must yield the same result.', + ); + + // a sanity check to ensure we don't compare ambiguous infinities + assert( + isFinite(originalFunction(firstArgument, secondArgument, thirdArgument)), + 'Function results must be finite.', + ); + } + }); +}); diff --git a/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 4e95ff4b118..c8f8acd1fab 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -13,11 +13,11 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from '../../../speech/common/speechService.js'; -import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider, IChatWelcomeMessageContent } from '../../common/chatAgents.js'; +import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider } from '../../common/chatAgents.js'; import { IChatModel } from '../../common/chatModel.js'; import { IChatFollowup, IChatProgress } from '../../common/chatService.js'; +import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from '../../common/voiceChatService.js'; -import { ChatAgentLocation } from '../../common/constants.js'; suite('VoiceChat', () => { @@ -32,6 +32,7 @@ suite('VoiceChat', () => { extensionDisplayName = ''; extensionPublisherId = ''; locations: ChatAgentLocation[] = [ChatAgentLocation.Panel]; + modes = [ChatMode.Ask]; public readonly name: string; constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { this.name = id; @@ -50,7 +51,6 @@ suite('VoiceChat', () => { throw new Error('Method not implemented.'); } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideWelcomeMessage?(token: CancellationToken): ProviderResult { throw new Error('Method not implemented.'); } metadata = {}; } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index e8aedfe3cc5..3801ee2391e 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import './emptyTextEditorHint.css'; -import * as dom from '../../../../../base/browser/dom.js'; -import { DisposableStore, dispose, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { $, addDisposableListener, getActiveWindow } from '../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../../editor/browser/editorBrowser.js'; import { localize } from '../../../../../nls.js'; import { ChangeLanguageAction } from '../../../../browser/parts/editor/editorStatus.js'; @@ -24,80 +24,60 @@ import { ApplyFileSnippetAction } from '../../../snippets/browser/commands/fileT import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; -import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { KeybindingLabel } from '../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { OS } from '../../../../../base/common/platform.js'; import { status } from '../../../../../base/browser/ui/aria/aria.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from '../../../../services/output/common/output.js'; import { SEARCH_RESULT_LANGUAGE_ID } from '../../../../services/search/common/search.js'; -import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; - -const $ = dom.$; - -export interface IEmptyTextEditorHintOptions { - readonly clickable?: boolean; -} +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; export const emptyTextEditorHintSetting = 'workbench.editor.empty.hint'; -export class EmptyTextEditorHintContribution implements IEditorContribution { +export class EmptyTextEditorHintContribution extends Disposable implements IEditorContribution { - public static readonly ID = 'editor.contrib.emptyTextEditorHint'; + static readonly ID = 'editor.contrib.emptyTextEditorHint'; - protected toDispose: IDisposable[]; private textHintContentWidget: EmptyTextEditorHintContentWidget | undefined; constructor( protected readonly editor: ICodeEditor, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, - @ICommandService private readonly commandService: ICommandService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @IHoverService protected readonly hoverService: IHoverService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInlineChatSessionService private readonly inlineChatSessionService: IInlineChatSessionService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IProductService protected readonly productService: IProductService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IInstantiationService private readonly instantiationService: IInstantiationService ) { - this.toDispose = []; - this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelContent(() => this.update())); - this.toDispose.push(this.chatAgentService.onDidChangeAgents(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); - this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + super(); + + this._register(this.editor.onDidChangeModel(() => this.update())); + this._register(this.editor.onDidChangeModelLanguage(() => this.update())); + this._register(this.editor.onDidChangeModelContent(() => this.update())); + this._register(this.chatAgentService.onDidChangeAgents(() => this.update())); + this._register(this.editor.onDidChangeModelDecorations(() => this.update())); + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.readOnly)) { this.update(); } })); - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(emptyTextEditorHintSetting)) { this.update(); } })); - this.toDispose.push(inlineChatSessionService.onWillStartSession(editor => { + this._register(inlineChatSessionService.onWillStartSession(editor => { if (this.editor === editor) { this.textHintContentWidget?.dispose(); } })); - this.toDispose.push(inlineChatSessionService.onDidEndSession(e => { + this._register(inlineChatSessionService.onDidEndSession(e => { if (this.editor === e.editor) { this.update(); } })); } - protected _getOptions(): IEmptyTextEditorHintOptions { - return { clickable: true }; - } - - protected _shouldRenderHint() { + protected shouldRenderHint() { const configValue = this.configurationService.getValue(emptyTextEditorHintSetting); if (configValue === 'hidden') { return false; @@ -137,63 +117,49 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { } protected update(): void { - const shouldRenderHint = this._shouldRenderHint(); + const shouldRenderHint = this.shouldRenderHint(); if (shouldRenderHint && !this.textHintContentWidget) { - this.textHintContentWidget = new EmptyTextEditorHintContentWidget( - this.editor, - this._getOptions(), - this.editorGroupsService, - this.commandService, - this.configurationService, - this.hoverService, - this.keybindingService, - this.chatAgentService, - this.telemetryService, - this.productService, - this.contextMenuService - ); + this.textHintContentWidget = this.instantiationService.createInstance(EmptyTextEditorHintContentWidget, this.editor); } else if (!shouldRenderHint && this.textHintContentWidget) { this.textHintContentWidget.dispose(); this.textHintContentWidget = undefined; } } - dispose(): void { - dispose(this.toDispose); + override dispose(): void { + super.dispose(); + this.textHintContentWidget?.dispose(); } } -class EmptyTextEditorHintContentWidget implements IContentWidget { +class EmptyTextEditorHintContentWidget extends Disposable implements IContentWidget { private static readonly ID = 'editor.widget.emptyHint'; private domNode: HTMLElement | undefined; - private readonly toDispose: DisposableStore; private isVisible = false; private ariaLabel: string = ''; constructor( private readonly editor: ICodeEditor, - private readonly options: IEmptyTextEditorHintOptions, - private readonly editorGroupsService: IEditorGroupsService, - private readonly commandService: ICommandService, - private readonly configurationService: IConfigurationService, - private readonly hoverService: IHoverService, - private readonly keybindingService: IKeybindingService, - private readonly chatAgentService: IChatAgentService, - private readonly telemetryService: ITelemetryService, - private readonly productService: IProductService, - private readonly contextMenuService: IContextMenuService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { - this.toDispose = new DisposableStore(); - this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + super(); + + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (this.domNode && e.hasChanged(EditorOption.fontInfo)) { this.editor.applyFontInfo(this.domNode); } })); const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500); - this.toDispose.add(onDidFocusEditorText(() => { + this._register(onDidFocusEditorText(() => { if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EmptyEditorHint)) { status(this.ariaLabel); } @@ -205,7 +171,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } - private _disableHint(e?: MouseEvent) { + private disableHint(e?: MouseEvent) { const disableHint = () => { this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); this.dispose(); @@ -218,7 +184,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } this.contextMenuService.showContextMenu({ - getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getAnchor: () => { return new StandardMouseEvent(getActiveWindow(), e); }, getActions: () => { return [{ id: 'workench.action.disableEmptyEditorHint', @@ -235,112 +201,39 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { }); } - private _getHintInlineChat(providers: IChatAgent[]) { - const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; - - const inlineChatId = 'inlineChat.start'; - let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; - - const handleClick = () => { - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'inlineChat.hintAction', - from: 'hint' - }); - this.commandService.executeCommand(inlineChatId, { from: 'hint' }); - }; - - const hintHandler: IContentActionHandler = { - disposables: this.toDispose, - callback: (index, _event) => { - switch (index) { - case '0': - handleClick(); - break; - } - } - }; - - const hintElement = $('empty-hint-text'); - hintElement.style.display = 'block'; - - const keybindingHint = this.keybindingService.lookupKeybinding(inlineChatId); - const keybindingHintLabel = keybindingHint?.getLabel(); - - if (keybindingHint && keybindingHintLabel) { - const actionPart = localize('emptyHintText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName); - - const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { - if (this.options.clickable) { - const hintPart = $('a', undefined, fragment); - hintPart.style.fontStyle = 'italic'; - hintPart.style.cursor = 'pointer'; - this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); - this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); - return hintPart; - } else { - const hintPart = $('span', undefined, fragment); - hintPart.style.fontStyle = 'italic'; - return hintPart; - } - }); - - hintElement.appendChild(before); - - const label = hintHandler.disposables.add(new KeybindingLabel(hintElement, OS)); - label.set(keybindingHint); - label.element.style.width = 'min-content'; - label.element.style.display = 'inline'; - - if (this.options.clickable) { - label.element.style.cursor = 'pointer'; - this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); - this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); - } - - hintElement.appendChild(after); - - const typeToDismiss = localize('emptyHintTextDismiss', 'Start typing to dismiss.'); - const textHint2 = $('span', undefined, typeToDismiss); - textHint2.style.fontStyle = 'italic'; - hintElement.appendChild(textHint2); - - ariaLabel = actionPart.concat(typeToDismiss); - } else { - const hintMsg = localize({ - key: 'inlineChatHint', - comment: [ - 'Preserve double-square brackets and their order', - ] - }, '[[Ask {0} to do something]] or start typing to dismiss.', providerName); - const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler }); - hintElement.appendChild(rendered); - } - - return { ariaLabel, hintElement }; - } + private getHint() { + const hasInlineChatProvider = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)).length > 0; - private _getHintDefault() { const hintHandler: IContentActionHandler = { - disposables: this.toDispose, + disposables: this._store, callback: (index, event) => { switch (index) { case '0': - languageOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? askSomething(event.browserEvent) : languageOnClickOrTap(event.browserEvent); break; case '1': - snippetOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? languageOnClickOrTap(event.browserEvent) : snippetOnClickOrTap(event.browserEvent); break; case '2': - chooseEditorOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? snippetOnClickOrTap(event.browserEvent) : chooseEditorOnClickOrTap(event.browserEvent); break; case '3': - this._disableHint(); + this.disableHint(); break; } } }; // the actual command handlers... + const askSomethingCommandId = 'inlineChat.start'; + const askSomething = async (e: UIEvent) => { + e.stopPropagation(); + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: askSomethingCommandId, + from: 'hint' + }); + await this.commandService.executeCommand(askSomethingCommandId, { from: 'hint' }); + }; const languageOnClickOrTap = async (e: UIEvent) => { e.stopPropagation(); // Need to focus editor before so current editor becomes active and the command is properly executed @@ -379,28 +272,33 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } }; - const hintMsg = localize({ - key: 'message', + const keybindingsLookup = hasInlineChatProvider ? [askSomethingCommandId, ChangeLanguageAction.ID, ApplyFileSnippetAction.Id] : [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; + const keybindingLabels = keybindingsLookup.map(id => this.keybindingService.lookupKeybinding(id)?.getLabel()); + + const hintMsg = (hasInlineChatProvider ? localize({ + key: 'emptyTextEditorHintWithInlineChat', + comment: [ + 'Preserve double-square brackets and their order', + 'language refers to a programming language' + ] + }, '[[Open chat]] ({0}), or [[select a language]] ({1}), or [[fill with template]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '') : localize({ + key: 'emptyTextEditorHintWithoutInlineChat', comment: [ 'Preserve double-square brackets and their order', 'language refers to a programming language' ] - }, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.'); + }, '[[Select a language]] ({0}), or [[fill with template]] ({1}), or [[open a different editor]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '')).replaceAll('()', ''); const hintElement = renderFormattedText(hintMsg, { actionHandler: hintHandler, renderCodeSegments: false, }); hintElement.style.fontStyle = 'italic'; - // ugly way to associate keybindings... - const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; - const keybindingLabels = keybindingsLookup.map((id) => this.keybindingService.lookupKeybinding(id)?.getLabel() ?? id); - const ariaLabel = localize('defaultHintAriaLabel', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels); + const ariaLabel = hasInlineChatProvider ? + localize('defaultHintAriaLabelWithInlineChat', 'Execute {0} to ask a question, execute {1} to select a language, or execute {2} to fill with template and get started. Start typing to dismiss.', ...keybindingLabels) : + localize('defaultHintAriaLabelWithoutInlineChat', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels); for (const anchor of hintElement.querySelectorAll('a')) { anchor.style.cursor = 'pointer'; - const id = keybindingsLookup.shift(); - const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - hintHandler.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; @@ -412,12 +310,11 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; - const inlineChatProviders = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)); - const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); + const { hintElement, ariaLabel } = this.getHint(); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); - this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => { + this._register(addDisposableListener(this.domNode, 'click', () => { this.editor.focus(); })); @@ -434,9 +331,10 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { }; } - dispose(): void { + override dispose(): void { + super.dispose(); + this.editor.removeContentWidget(this); - dispose(this.toDispose); } } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 972fd363b90..37963ed5fba 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/code/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/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 037908fa8db..d8dea5ca723 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/code/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/code/src/vs/workbench/contrib/comments/browser/commentNode.ts b/code/src/vs/workbench/contrib/comments/browser/commentNode.ts index 77f5e4117a2..fbd9e81aa39 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -44,7 +44,6 @@ import { CommentContextKeys } from '../common/commentContextKeys.js'; import { FileAccess, Schemas } from '../../../../base/common/network.js'; import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { MarshalledCommentThread } from '../../../common/comments.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; @@ -116,7 +115,6 @@ export class CommentNode extends Disposable { @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService private configurationService: IConfigurationService, @IHoverService private hoverService: IHoverService, - @IAccessibilityService private accessibilityService: IAccessibilityService, @IKeybindingService private keybindingService: IKeybindingService, @ITextModelService private readonly textModelService: ITextModelService, ) { @@ -160,9 +158,6 @@ export class CommentNode extends Disposable { if (pendingEdit) { this.switchToEditMode(); } - this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { - this.toggleToolbarHidden(true); - })); this.activeCommentListeners(); } @@ -223,7 +218,7 @@ export class CommentNode extends Disposable { private updateCommentUserIcon(userIconPath: UriComponents | undefined) { this._avatar.textContent = ''; if (userIconPath) { - const img = dom.append(this._avatar, dom.$('img.avatar')); + const img = dom.append(this._avatar, dom.$('img.avatar')) as HTMLImageElement; img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); img.onerror = _ => img.remove(); } @@ -271,18 +266,9 @@ export class CommentNode extends Disposable { } this._actionsToolbarContainer = dom.append(header, dom.$('.comment-actions')); - this.toggleToolbarHidden(true); this.createActionsToolbar(); } - private toggleToolbarHidden(hidden: boolean) { - if (hidden && !this.accessibilityService.isScreenReaderOptimized()) { - this._actionsToolbarContainer.classList.add('hidden'); - } else { - this._actionsToolbarContainer.classList.remove('hidden'); - } - } - private getToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { const contributedActions = menu.getActions({ shouldForwardArgs: true }); const primary: IAction[] = []; @@ -328,19 +314,11 @@ export class CommentNode extends Disposable { this.toolbar.value.context = this.commentNodeContext; this.toolbar.value.actionRunner = this._actionRunner; - - this.registerActionBarListeners(this._actionsToolbarContainer); } private createActionsToolbar() { const actions: IAction[] = []; - const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); - const toggleReactionAction = hasReactionHandler ? this.createReactionPicker(this.comment.commentReactions || []) : undefined; - if (toggleReactionAction) { - actions.push(toggleReactionAction); - } - const menu = this._commentMenus.getCommentTitleActions(this.comment, this._contextKeyService); this._register(menu); this._register(menu.onDidChange(e => { @@ -348,9 +326,6 @@ export class CommentNode extends Disposable { if (!this.toolbar && (primary.length || secondary.length)) { this.createToolbar(); } - if (toggleReactionAction) { - primary.unshift(toggleReactionAction); - } this.toolbar.value!.setActions(primary, secondary); })); @@ -680,7 +655,6 @@ export class CommentNode extends Disposable { setFocus(focused: boolean, visible: boolean = false) { if (focused) { this._domNode.focus(); - this.toggleToolbarHidden(false); this._actionsToolbarContainer.classList.add('tabfocused'); this._domNode.tabIndex = 0; if (this.comment.mode === languages.CommentMode.Editing) { @@ -688,26 +662,12 @@ export class CommentNode extends Disposable { } } else { if (this._actionsToolbarContainer.classList.contains('tabfocused') && !this._actionsToolbarContainer.classList.contains('mouseover')) { - this.toggleToolbarHidden(true); this._domNode.tabIndex = -1; } this._actionsToolbarContainer.classList.remove('tabfocused'); } } - private registerActionBarListeners(actionsContainer: HTMLElement): void { - this._register(dom.addDisposableListener(this._domNode, 'mouseenter', () => { - this.toggleToolbarHidden(false); - actionsContainer.classList.add('mouseover'); - })); - this._register(dom.addDisposableListener(this._domNode, 'mouseleave', () => { - if (actionsContainer.classList.contains('mouseover') && !actionsContainer.classList.contains('tabfocused')) { - this.toggleToolbarHidden(true); - } - actionsContainer.classList.remove('mouseover'); - })); - } - async update(newComment: languages.Comment) { if (newComment.body !== this.comment.body) { diff --git a/code/src/vs/workbench/contrib/comments/browser/commentReply.ts b/code/src/vs/workbench/contrib/comments/browser/commentReply.ts index 917b6a64239..b181a9cb5c1 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -9,7 +9,7 @@ import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mo import { IAction } from '../../../../base/common/actions.js'; import { Disposable, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; -import { Schemas } from '../../../../base/common/network.js'; +import { FileAccess, Schemas } from '../../../../base/common/network.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -38,8 +38,10 @@ export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; export class CommentReply extends Disposable { commentEditor: ICodeEditor; - form: HTMLElement; + private _container: HTMLElement; + private _form: HTMLElement; commentEditorIsEmpty: IContextKey; + private avatar!: HTMLElement; private _error!: HTMLElement; private _formActions!: HTMLElement; private _editorActions!: HTMLElement; @@ -70,16 +72,18 @@ export class CommentReply extends Disposable { @ITextModelService private readonly textModelService: ITextModelService ) { super(); - - this.form = dom.append(container, dom.$('.comment-form')); - this.commentEditor = this._register(this._scopedInstatiationService.createInstance(SimpleCommentEditor, this.form, SimpleCommentEditor.getEditorOptions(configurationService), _contextKeyService, this._parentThread)); + this._container = dom.append(container, dom.$('.comment-form-container')); + this._form = dom.append(this._container, dom.$('.comment-form')); + this.commentEditor = this._register(this._scopedInstatiationService.createInstance(SimpleCommentEditor, this._form, SimpleCommentEditor.getEditorOptions(configurationService), _contextKeyService, this._parentThread)); this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); this.commentEditorIsEmpty.set(!this._pendingComment); this.initialize(focus); } - async initialize(focus: boolean) { + private async initialize(focus: boolean) { + this.avatar = dom.append(this._form, dom.$('.avatar-container')); + this.updateAuthorInfo(); const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ @@ -115,7 +119,7 @@ export class CommentReply extends Disposable { } })); - this.createTextModelListener(this.commentEditor, this.form); + this.createTextModelListener(this.commentEditor, this._form); this.setCommentEditorDecorations(); @@ -123,12 +127,12 @@ export class CommentReply extends Disposable { if (this._pendingComment) { this.expandReplyArea(); } else if (hasExistingComments) { - this.createReplyButton(this.commentEditor, this.form); + this.createReplyButton(this.commentEditor, this._form); } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { this.expandReplyArea(); } - this._error = dom.append(this.form, dom.$('.validation-error.hidden')); - const formActions = dom.append(this.form, dom.$('.form-actions')); + this._error = dom.append(this._container, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._container, dom.$('.form-actions')); this._formActions = dom.append(formActions, dom.$('.other-actions')); this.createCommentWidgetFormActions(this._formActions, model.object.textEditorModel); this._editorActions = dom.append(formActions, dom.$('.editor-actions')); @@ -149,7 +153,7 @@ export class CommentReply extends Disposable { const oldAndNewBothEmpty = !this._commentThread.comments?.length && !commentThread.comments?.length; if (!this._reviewThreadReplyButton) { - this.createReplyButton(this.commentEditor, this.form); + this.createReplyButton(this.commentEditor, this._form); } if (this._commentThread.comments && this._commentThread.comments.length === 0 && !oldAndNewBothEmpty) { @@ -203,11 +207,23 @@ export class CommentReply extends Disposable { return this.commentEditor.hasWidgetFocus(); } + private updateAuthorInfo() { + this.avatar.textContent = ''; + if (typeof this._commentThread.canReply !== 'boolean' && this._commentThread.canReply.iconPath) { + this.avatar.style.display = 'block'; + const img = dom.append(this.avatar, dom.$('img.avatar')) as HTMLImageElement; + img.src = FileAccess.uriToBrowserUri(URI.revive(this._commentThread.canReply.iconPath)).toString(true); + } else { + this.avatar.style.display = 'none'; + } + } + public updateCanReply() { + this.updateAuthorInfo(); if (!this._commentThread.canReply) { - this.form.style.display = 'none'; + this._container.style.display = 'none'; } else { - this.form.style.display = 'block'; + this._container.style.display = 'block'; } } @@ -320,12 +336,12 @@ export class CommentReply extends Disposable { } private get isReplyExpanded(): boolean { - return this.form.classList.contains('expand'); + return this._container.classList.contains('expand'); } private expandReplyArea() { if (!this.isReplyExpanded) { - this.form.classList.add('expand'); + this._container.classList.add('expand'); this.commentEditor.focus(); this.commentEditor.layout(); } @@ -345,7 +361,7 @@ export class CommentReply extends Disposable { } this.commentEditor.setValue(''); this._pendingComment = { body: '', cursor: new Position(1, 1) }; - this.form.classList.remove('expand'); + this._container.classList.remove('expand'); this._error.textContent = ''; this._error.classList.add('hidden'); } diff --git a/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 0039a4030ea..72f3675524f 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -338,8 +338,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (confirmSetting === 'whenHasUnsubmittedComments' && this._commentThreadWidget.hasUnsubmittedComments) { const result = await this.dialogService.confirm({ - message: nls.localize('confirmCollapse', "Collapsing a comment thread will discard unsubmitted comments. Do you want to collapse this comment thread?"), - primaryButton: nls.localize('collapse', "Collapse"), + message: nls.localize('confirmCollapse', "Collapsing this comment thread will discard unsubmitted comments. Are you sure you want to discard these comments?"), + primaryButton: nls.localize('discard', "Discard"), type: Severity.Warning, checkbox: { label: nls.localize('neverAskAgain', "Never ask me again"), checked: false } }); diff --git a/code/src/vs/workbench/contrib/comments/browser/media/review.css b/code/src/vs/workbench/contrib/comments/browser/media/review.css index e062349f9de..f0b9a064d8d 100644 --- a/code/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/code/src/vs/workbench/contrib/comments/browser/media/review.css @@ -78,7 +78,7 @@ margin-top: 4px !important; } -.review-widget .body .review-comment .avatar-container img.avatar { +.review-widget .body .avatar-container img.avatar { height: 28px; width: 28px; display: inline-block; @@ -172,7 +172,6 @@ } .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { - display: none; background-size: 16px; font-size: 16px; width: 26px; @@ -183,11 +182,6 @@ border: none; } -.review-widget .body .review-comment .review-comment-contents:hover .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { - display: inline-block; - background-size: 16px; -} - .review-widget .body .review-comment .comment-title .action-label { display: block; height: 16px; @@ -252,7 +246,7 @@ max-width: 100%; } -.review-widget .body .comment-form { +.review-widget .body .comment-form-container { margin: 8px 20px; } @@ -302,7 +296,6 @@ padding: 4px 10px; } - .review-widget .body .comment-additional-actions .codicon-drop-down-button { align-items: center; } @@ -310,17 +303,27 @@ .review-widget .body .monaco-editor { color: var(--vscode-editor-foreground); } -.review-widget .body .comment-form.expand .review-thread-reply-button { + +.review-widget .body .comment-form-container .comment-form { + display: flex; + flex-direction: row; +} + +.review-widget .body .comment-form-container .comment-form .avatar-container { + padding-right: 20px; +} + +.review-widget .body .comment-form-container.expand .review-thread-reply-button { display: none; } -.review-widget .body .comment-form.expand .monaco-editor, -.review-widget .body .comment-form.expand .form-actions { +.review-widget .body .comment-form-container.expand .monaco-editor, +.review-widget .body .comment-form-container.expand .form-actions { display: block; box-sizing: content-box; } -.review-widget .body .comment-form .review-thread-reply-button { +.review-widget .body .comment-form-container .review-thread-reply-button { text-align: left; display: block; width: 100%; @@ -339,18 +342,18 @@ font-family: var(--monaco-monospace-font); } -.review-widget .body .comment-form .review-thread-reply-button:focus { +.review-widget .body .comment-form-container .review-thread-reply-button:focus { outline-style: solid; outline-width: 1px; } -.review-widget .body .comment-form .monaco-editor, -.review-widget .body .comment-form .monaco-editor .monaco-editor-background, +.review-widget .body .comment-form-container .monaco-editor, +.review-widget .body .comment-form-container .monaco-editor .monaco-editor-background, .review-widget .body .edit-container .monaco-editor .monaco-editor-background { background-color: var(--vscode-editorCommentsWidget-replyInputBackground); } -.review-widget .body .comment-form .monaco-editor, +.review-widget .body .comment-form-container .monaco-editor, .review-widget .body .edit-container .monaco-editor { width: 100%; min-height: 90px; @@ -361,12 +364,12 @@ padding: 6px 0 6px 12px; } -.review-widget .body .comment-form .monaco-editor, -.review-widget .body .comment-form .form-actions { +.review-widget .body .comment-form-container .monaco-editor, +.review-widget .body .comment-form-container .form-actions { display: none; } -.review-widget .body .comment-form .form-actions, +.review-widget .body .comment-form-container .form-actions, .review-widget .body .edit-container .form-actions { overflow: auto; margin: 10px 0; @@ -381,7 +384,7 @@ margin-right: 12px; } -.review-widget .body .comment-form .form-actions .monaco-text-button, +.review-widget .body .comment-form-container .form-actions .monaco-text-button, .review-widget .body .edit-container .monaco-text-button { width: auto; padding: 4px 10px; diff --git a/code/src/vs/workbench/contrib/debug/browser/callStackView.ts b/code/src/vs/workbench/contrib/debug/browser/callStackView.ts index 28d99d7646f..176b0aa7006 100644 --- a/code/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -189,7 +189,6 @@ export class CallStackView extends ViewPane { this.updateActions(); this.needsRefresh = false; - this.dataSource.deemphasizedStackFramesToShow = []; await this.tree.updateChildren(); try { const toExpand = new Set(); @@ -326,7 +325,7 @@ export class CallStackView extends ViewPane { } } if (element instanceof Array) { - this.dataSource.deemphasizedStackFramesToShow.push(...element); + element.forEach(sf => this.dataSource.deemphasizedStackFramesToShow.add(sf)); this.tree.updateChildren(); } })); @@ -509,6 +508,7 @@ interface IStackFrameTemplateData { label: HighlightedLabel; actionBar: ActionBar; templateDisposable: DisposableStore; + elementDisposables: DisposableStore; } function getSessionContextOverlay(session: IDebugSession): [string, any][] { @@ -726,11 +726,12 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, index: number, data: IStackFrameTemplateData): void { @@ -744,7 +745,7 @@ class StackFramesRenderer implements ICompressibleTreeRenderer { + const action = data.elementDisposables.add(new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { await stackFrame.restart(); } catch (e) { this.notificationService.error(e); } - }); + })); data.actionBar.push(action, { icon: true, label: false }); } } @@ -774,9 +775,12 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, FuzzyScore>, index: number, templateData: IStackFrameTemplateData, height: number | undefined): void { throw new Error('Method not implemented.'); } + disposeElement(element: ITreeNode, index: number, templateData: IStackFrameTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + } disposeTemplate(templateData: IStackFrameTemplateData): void { - templateData.actionBar.dispose(); + templateData.templateDisposable.dispose(); } } @@ -929,7 +933,7 @@ function isDebugSession(obj: any): obj is IDebugSession { } class CallStackDataSource implements IAsyncDataSource { - deemphasizedStackFramesToShow: IStackFrame[] = []; + deemphasizedStackFramesToShow = new WeakSet(); constructor(private debugService: IDebugService) { } @@ -977,7 +981,7 @@ class CallStackDataSource implements IAsyncDataSource { if (child instanceof StackFrame && child.source && isFrameDeemphasized(child)) { // Check if the user clicked to show the deemphasized source - if (this.deemphasizedStackFramesToShow.indexOf(child) === -1) { + if (!this.deemphasizedStackFramesToShow.has(child)) { if (result.length) { const last = result[result.length - 1]; if (last instanceof Array) { diff --git a/code/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/code/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 1c522165c23..fd9120ac2d7 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -40,7 +40,7 @@ import { BreakpointsView } from './breakpointsView.js'; import { CallStackEditorContribution } from './callStackEditorContribution.js'; import { CallStackView } from './callStackView.js'; import { registerColors } from './debugColors.js'; -import { ADD_CONFIGURATION_ID, ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTINUE_ID, CONTINUE_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_STACK_TRACE_ID, COPY_VALUE_ID, COPY_VALUE_LABEL, DEBUG_COMMAND_CATEGORY, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, EDIT_EXPRESSION_COMMAND_ID, JUMP_TO_CURSOR_ID, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, PAUSE_ID, PAUSE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, REMOVE_EXPRESSION_COMMAND_ID, RESTART_FRAME_ID, RESTART_LABEL, RESTART_SESSION_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL, SET_EXPRESSION_COMMAND_ID, SHOW_LOADED_SCRIPTS_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL, TERMINATE_THREAD_ID, TOGGLE_INLINE_BREAKPOINT_ID } from './debugCommands.js'; +import { ADD_CONFIGURATION_ID, ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTINUE_ID, CONTINUE_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_STACK_TRACE_ID, COPY_VALUE_ID, COPY_VALUE_LABEL, DEBUG_COMMAND_CATEGORY, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, EDIT_EXPRESSION_COMMAND_ID, JUMP_TO_CURSOR_ID, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, PAUSE_ID, PAUSE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, REMOVE_EXPRESSION_COMMAND_ID, RESTART_FRAME_ID, RESTART_LABEL, RESTART_SESSION_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL, SET_EXPRESSION_COMMAND_ID, SHOW_LOADED_SCRIPTS_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL, TERMINATE_THREAD_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_ADDRESS_ID, COPY_ADDRESS_LABEL, TOGGLE_BREAKPOINT_ID } from './debugCommands.js'; import { DebugConsoleQuickAccess } from './debugConsoleQuickAccess.js'; import { RunToCursorAction, SelectionToReplAction, SelectionToWatchExpressionsAction } from './debugEditorActions.js'; import { DebugEditorContribution } from './debugEditorContribution.js'; @@ -381,6 +381,28 @@ MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { when: CONTEXT_DEBUGGERS_AVAILABLE }); +// Disassembly + +MenuRegistry.appendMenuItem(MenuId.DebugDisassemblyContext, { + group: '1_edit', + command: { + id: COPY_ADDRESS_ID, + title: COPY_ADDRESS_LABEL, + }, + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE +}); + +MenuRegistry.appendMenuItem(MenuId.DebugDisassemblyContext, { + group: '3_breakpoints', + command: { + id: TOGGLE_BREAKPOINT_ID, + title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle Breakpoint"), + }, + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE +}); + // Breakpoint actions are registered from breakpointsView.ts // Install Debuggers diff --git a/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 92b79ee572b..fe5bb95a0a5 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -40,6 +40,8 @@ import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; +export const COPY_ADDRESS_ID = 'editor.debug.action.copyAddress'; +export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; export const COPY_STACK_TRACE_ID = 'debug.copyStackTrace'; export const REVERSE_CONTINUE_ID = 'workbench.action.debug.reverseContinue'; @@ -105,6 +107,7 @@ export const CALLSTACK_UP_LABEL = nls.localize2('callStackUp', "Navigate Up Call export const CALLSTACK_DOWN_LABEL = nls.localize2('callStackDown', "Navigate Down Call Stack"); export const COPY_EVALUATE_PATH_LABEL = nls.localize2('copyAsExpression', "Copy as Expression"); export const COPY_VALUE_LABEL = nls.localize2('copyValue', "Copy Value"); +export const COPY_ADDRESS_LABEL = nls.localize2('copyAddress', "Copy Address"); export const ADD_TO_WATCH_LABEL = nls.localize2('addToWatchExpressions', "Add to Watch"); export const SELECT_DEBUG_CONSOLE_LABEL = nls.localize2('selectDebugConsole', "Select Debug Console"); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index f6a5af3713a..81b57c8b357 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -25,18 +25,19 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { PanelFocusContext } from '../../../common/contextkeys.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { openBreakpointSource } from './breakpointsView.js'; -import { DisassemblyView } from './disassemblyView.js'; +import { DisassemblyView, IDisassembledInstructionEntry } from './disassemblyView.js'; import { Repl } from './repl.js'; import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXCEPTION_WIDGET_VISIBLE, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_IN_DEBUG_MODE, CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugConfiguration, IDebugEditorContribution, IDebugService, REPL_VIEW_ID, WATCH_VIEW_ID } from '../common/debug.js'; import { getEvaluatableExpressionAtPosition } from '../common/debugUtils.js'; import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { TOGGLE_BREAKPOINT_ID } from '../../../../workbench/contrib/debug/browser/debugCommands.js'; class ToggleBreakpointAction extends Action2 { constructor() { super({ - id: 'editor.debug.action.toggleBreakpoint', + id: TOGGLE_BREAKPOINT_ID, title: { ...nls.localize2('toggleBreakpointAction', "Debug: Toggle Breakpoint"), mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint"), @@ -57,13 +58,13 @@ class ToggleBreakpointAction extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry): Promise { const editorService = accessor.get(IEditorService); const debugService = accessor.get(IDebugService); const activePane = editorService.activeEditorPane; if (activePane instanceof DisassemblyView) { - const location = activePane.focusedAddressAndOffset; + const location = entry ? activePane.getAddressAndOffset(entry) : activePane.focusedAddressAndOffset; if (location) { const bps = debugService.getModel().getInstructionBreakpoints(); const toRemove = bps.find(bp => bp.address === location.address); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/code/src/vs/workbench/contrib/debug/browser/debugMemory.ts index 8fcd11962b3..9eabf24fed7 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugMemory.ts +++ b/code/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/code/src/vs/workbench/contrib/debug/browser/debugSession.ts b/code/src/vs/workbench/contrib/debug/browser/debugSession.ts index 3e63e489167..7054f7ad505 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/code/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/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index c98e35153df..0f15bf5d2e7 100644 --- a/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/code/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -6,7 +6,7 @@ import { PixelRatio } from '../../../../base/browser/pixelRatio.js'; import { $, Dimension, addStandardDisposableListener, append } from '../../../../base/browser/dom.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; -import { ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js'; +import { ITableContextMenuEvent, ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js'; import { binarySearch2 } from '../../../../base/common/arrays.js'; import { Color } from '../../../../base/common/color.js'; import { Emitter } from '../../../../base/common/event.js'; @@ -25,7 +25,7 @@ import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchTable } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; @@ -43,8 +43,14 @@ import { getUriFromSource } from '../common/debugSource.js'; import { isUri, sourcesEqual } from '../common/debugUtils.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; - -interface IDisassembledInstructionEntry { +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { COPY_ADDRESS_ID, COPY_ADDRESS_LABEL } from '../../../../workbench/contrib/debug/browser/debugCommands.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; + +export interface IDisassembledInstructionEntry { allowBreakpoint: boolean; isBreakpointSet: boolean; isBreakpointEnabled: boolean; @@ -62,7 +68,6 @@ interface IDisassembledInstructionEntry { address: bigint; } - // Special entry as a placeholer when disassembly is not available const disassemblyNotAvailable: IDisassembledInstructionEntry = { allowBreakpoint: false, @@ -91,6 +96,7 @@ export class DisassemblyView extends EditorPane { private _enableSourceCodeRender: boolean = true; private _loadingLock: boolean = false; private readonly _referenceToMemoryAddress = new Map(); + private menu: IMenu; constructor( group: IEditorGroup, @@ -100,9 +106,14 @@ export class DisassemblyView extends EditorPane { @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); + this.menu = menuService.createMenu(MenuId.DebugDisassemblyContext, contextKeyService); + this._register(this.menu); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); this._previousDebuggingState = _debugService.state; @@ -180,6 +191,10 @@ export class DisassemblyView extends EditorPane { return undefined; } + return this.getAddressAndOffset(element); + } + + getAddressAndOffset(element: IDisassembledInstructionEntry) { const reference = element.instructionReference; const offset = Number(element.address - this.getReferenceAddress(reference)!); return { reference, offset, address: element.address }; @@ -273,6 +288,8 @@ export class DisassemblyView extends EditorPane { } })); + this._register(this._disassembledInstructions.onContextMenu(e => this.onContextMenu(e))); + this._register(this._debugService.getViewModel().onDidFocusStackFrame(({ stackFrame }) => { if (this._disassembledInstructions && stackFrame?.instructionPointerReference) { this.goToInstructionAndOffset(stackFrame.instructionPointerReference, 0); @@ -633,6 +650,15 @@ export class DisassemblyView extends EditorPane { this._referenceToMemoryAddress.clear(); this._disassembledInstructions?.splice(0, this._disassembledInstructions.length, [disassemblyNotAvailable]); } + + private onContextMenu(e: ITableContextMenuEvent): void { + const actions = getFlatContextMenuActions(this.menu.getActions({ shouldForwardArgs: true })); + this._contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + getActionsContext: () => e.element + }); + } } interface IBreakpointColumnTemplateData { @@ -1006,3 +1032,16 @@ export class DisassemblyViewContribution implements IWorkbenchContribution { this._onDidChangeModelLanguage?.dispose(); } } + +CommandsRegistry.registerCommand({ + metadata: { + description: COPY_ADDRESS_LABEL, + }, + id: COPY_ADDRESS_ID, + handler: async (accessor: ServicesAccessor, entry?: IDisassembledInstructionEntry) => { + if (entry?.instruction?.address) { + const clipboardService = accessor.get(IClipboardService); + clipboardService.writeText(entry.instruction.address); + } + } +}); diff --git a/code/src/vs/workbench/contrib/debug/common/debug.ts b/code/src/vs/workbench/contrib/debug/common/debug.ts index 6ce5e3e38a9..545504bfde0 100644 --- a/code/src/vs/workbench/contrib/debug/common/debug.ts +++ b/code/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/code/src/vs/workbench/contrib/debug/common/debugModel.ts b/code/src/vs/workbench/contrib/debug/common/debugModel.ts index 75794f9dfd4..75062d56a1d 100644 --- a/code/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/code/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/code/src/vs/workbench/contrib/debug/common/debugStorage.ts b/code/src/vs/workbench/contrib/debug/common/debugStorage.ts index 5c2a8f485ba..051b1bd0958 100644 --- a/code/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/code/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/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 8d3c433daac..4ce86561a40 100644 --- a/code/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/code/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/code/src/vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution.ts b/code/src/vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution.ts deleted file mode 100644 index 6c1899f0180..00000000000 --- a/code/src/vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Action } from '../../../../base/common/actions.js'; -import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { isDefined } from '../../../../base/common/types.js'; -import { localize } from '../../../../nls.js'; -import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; -import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; - -class DeprecatedExtensionMigratorContribution { - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IStorageService private readonly storageService: IStorageService, - @INotificationService private readonly notificationService: INotificationService, - @IOpenerService private readonly openerService: IOpenerService - ) { - this.init().catch(onUnexpectedError); - } - - private async init(): Promise { - const bracketPairColorizerId = 'coenraads.bracket-pair-colorizer'; - - await this.extensionsWorkbenchService.queryLocal(); - const extension = this.extensionsWorkbenchService.installed.find(e => e.identifier.id === bracketPairColorizerId); - if ( - !extension || - ((extension.enablementState !== EnablementState.EnabledGlobally) && - (extension.enablementState !== EnablementState.EnabledWorkspace)) - ) { - return; - } - - const state = await this.getState(); - const disablementLogEntry = state.disablementLog.some(d => d.extensionId === bracketPairColorizerId); - - if (disablementLogEntry) { - return; - } - - state.disablementLog.push({ extensionId: bracketPairColorizerId, disablementDateTime: new Date().getTime() }); - await this.setState(state); - - await this.extensionsWorkbenchService.setEnablement(extension, EnablementState.DisabledGlobally); - - const nativeBracketPairColorizationEnabledKey = 'editor.bracketPairColorization.enabled'; - const bracketPairColorizationEnabled = !!this.configurationService.inspect(nativeBracketPairColorizationEnabledKey).user; - - this.notificationService.notify({ - message: localize('bracketPairColorizer.notification', "The extension 'Bracket pair Colorizer' got disabled because it was deprecated."), - severity: Severity.Info, - actions: { - primary: [ - new Action('', localize('bracketPairColorizer.notification.action.uninstall', "Uninstall Extension"), undefined, undefined, () => { - this.extensionsWorkbenchService.uninstall(extension); - }), - ], - secondary: [ - !bracketPairColorizationEnabled ? new Action('', localize('bracketPairColorizer.notification.action.enableNative', "Enable Native Bracket Pair Colorization"), undefined, undefined, () => { - this.configurationService.updateValue(nativeBracketPairColorizationEnabledKey, true, ConfigurationTarget.USER); - }) : undefined, - new Action('', localize('bracketPairColorizer.notification.action.showMoreInfo', "More Info"), undefined, undefined, () => { - this.openerService.open('https://github.com/microsoft/vscode/issues/155179'); - }), - ].filter(isDefined), - } - }); - } - - private readonly storageKey = 'deprecatedExtensionMigrator.state'; - - private async getState(): Promise { - const jsonStr = await this.storageService.get(this.storageKey, StorageScope.APPLICATION, ''); - if (jsonStr === '') { - return { disablementLog: [] }; - } - return JSON.parse(jsonStr) as State; - } - - private async setState(state: State): Promise { - const json = JSON.stringify(state); - await this.storageService.store(this.storageKey, json, StorageScope.APPLICATION, StorageTarget.USER); - } -} - -interface State { - disablementLog: { - extensionId: string; - disablementDateTime: number; - }[]; -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DeprecatedExtensionMigratorContribution, LifecyclePhase.Restored); diff --git a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index cb2e6e81555..4deb48381eb 100644 --- a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -68,6 +68,7 @@ import { EditSessionsStoreClient } from '../common/editSessionsStorageClient.js' import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceIdentityService } from '../../../services/workspaces/common/workspaceIdentityService.js'; import { hashAsync } from '../../../../base/common/hash.js'; +import { ResourceSet } from '../../../../base/common/map.js'; registerSingleton(IEditSessionsLogService, EditSessionsLogService, InstantiationType.Delayed); registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, InstantiationType.Delayed); @@ -685,6 +686,24 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo // Save all saveable editors before building edit session contents await this.editorService.saveAll(); + // Do a first pass over all repositories to ensure that the edit session identity is created for each. + // This may change the working changes that need to be stored later + const createdEditSessionIdentities = new ResourceSet(); + for (const repository of this.scmService.repositories) { + const changedResources = this.getChangedResources(repository); + if (!changedResources.size) { + continue; + } + for (const uri of changedResources) { + const workspaceFolder = this.contextService.getWorkspaceFolder(uri); + if (!workspaceFolder || createdEditSessionIdentities.has(uri)) { + continue; + } + createdEditSessionIdentities.add(uri); + await this.editSessionIdentityService.onWillCreateEditSessionIdentity(workspaceFolder, cancellationToken); + } + } + for (const repository of this.scmService.repositories) { // Look through all resource groups and compute which files were added/modified/deleted const trackedUris = this.getChangedResources(repository); // A URI might appear in more than one resource group @@ -703,8 +722,6 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo continue; } - await this.editSessionIdentityService.onWillCreateEditSessionIdentity(workspaceFolder, cancellationToken); - name = name ?? workspaceFolder.name; const relativeFilePath = relativePath(workspaceFolder.uri, uri) ?? uri.path; diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index f1c6794da02..4ef6258e840 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -74,7 +74,7 @@ class RecommendationsNotification extends Disposable { show(): void { if (!this.notificationHandle) { - this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { sticky: true, onCancel: () => this.cancelled = true })); + this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { sticky: true, priority: NotificationPriority.OPTIONAL, onCancel: () => this.cancelled = true })); } } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0cdf4bea98c..80b5ab6fa74 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,10 +8,10 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, SortBy, FilterType } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, SortBy, FilterType, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey } from '../common/extensions.js'; import { InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; @@ -81,6 +81,8 @@ import { IProductService } from '../../../../platform/product/common/productServ import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import product from '../../../../platform/product/common/product.js'; import { ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js'; +import { SearchExtensionsTool, SearchExtensionsToolData } from '../common/searchExtensionsTool.js'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -265,7 +267,7 @@ Registry.as(ConfigurationExtensions.Configuration) description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), default: true }, - 'extensions.verifySignature': { + [VerifyExtensionSignatureConfigKey]: { type: 'boolean', description: localize('extensions.verifySignature', "When enabled, extensions are verified to be signed before getting installed."), default: true, @@ -297,70 +299,6 @@ Registry.as(ConfigurationExtensions.Configuration) minimumVersion: '1.99', }, }, - [AllowedExtensionsConfigKey]: { - // Note: Type is set only to object because to support policies generation during build time, where single type is expected. - type: 'object', - markdownDescription: localize('extensions.allowed', "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. For more information on how to configure this setting, please visit the [Configure Allowed Extensions](https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions) section."), - default: '*', - defaultSnippets: [{ - body: {}, - description: localize('extensions.allowed.none', "No extensions are allowed."), - }, { - body: { - '*': true - }, - description: localize('extensions.allowed.all', "All extensions are allowed."), - }], - scope: ConfigurationScope.APPLICATION, - policy: { - name: 'AllowedExtensions', - minimumVersion: '1.96', - description: localize('extensions.allowed.policy', "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. More information: https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions"), - }, - additionalProperties: false, - patternProperties: { - '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { - anyOf: [ - { - type: ['boolean', 'string'], - enum: [true, false, 'stable'], - description: localize('extensions.allow.description', "Allow or disallow the extension."), - enumDescriptions: [ - localize('extensions.allowed.enable.desc', "Extension is allowed."), - localize('extensions.allowed.disable.desc', "Extension is not allowed."), - localize('extensions.allowed.disable.stable.desc', "Allow only stable versions of the extension."), - ], - }, - { - type: 'array', - items: { - type: 'string', - }, - description: localize('extensions.allow.version.description', "Allow or disallow specific versions of the extension. To specifcy a platform specific version, use the format `platform@1.2.3`, e.g. `win32-x64@1.2.3`. Supported platforms are `win32-x64`, `win32-arm64`, `linux-x64`, `linux-arm64`, `linux-armhf`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`"), - }, - ] - }, - '([a-z0-9A-Z][a-z0-9-A-Z]*)$': { - type: ['boolean', 'string'], - enum: [true, false, 'stable'], - description: localize('extension.publisher.allow.description', "Allow or disallow all extensions from the publisher."), - enumDescriptions: [ - localize('extensions.publisher.allowed.enable.desc', "All extensions from the publisher are allowed."), - localize('extensions.publisher.allowed.disable.desc', "All extensions from the publisher are not allowed."), - localize('extensions.publisher.allowed.disable.stable.desc', "Allow only stable versions of the extensions from the publisher."), - ], - }, - '\\*': { - type: 'boolean', - enum: [true, false], - description: localize('extensions.allow.all.description', "Allow or disallow all extensions."), - enumDescriptions: [ - localize('extensions.allow.all.enable', "Allow all extensions."), - localize('extensions.allow.all.disable', "Disallow all extensions.") - ], - } - } - } } }); @@ -515,7 +453,7 @@ CommandsRegistry.registerCommand({ throw new Error(localize('notInstalled', "Extension '{0}' is not installed. Make sure you use the full extension ID, including the publisher, e.g.: ms-dotnettools.csharp.", id)); } if (extensionToUninstall.isBuiltin) { - throw new Error(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); + throw new Error(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id)); } try { @@ -567,7 +505,8 @@ export const CONTEXT_HAS_REMOTE_SERVER = new RawContextKey('hasRemoteSe export const CONTEXT_HAS_WEB_SERVER = new RawContextKey('hasWebServer', false); const CONTEXT_GALLERY_SORT_CAPABILITIES = new RawContextKey('gallerySortCapabilities', ''); const CONTEXT_GALLERY_FILTER_CAPABILITIES = new RawContextKey('galleryFilterCapabilities', ''); -const CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED = new RawContextKey('galleryAllRepositorySigned', false); +const CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED = new RawContextKey('galleryAllPublicRepositorySigned', false); +const CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED = new RawContextKey('galleryAllPrivateRepositorySigned', false); const CONTEXT_GALLERY_HAS_EXTENSION_LINK = new RawContextKey('galleryHasExtensionLink', false); async function runAction(action: IAction): Promise { @@ -588,6 +527,7 @@ type IExtensionActionOptions = IAction2Options & { class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService, @@ -634,7 +574,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi private async registerGalleryCapabilitiesContexts(extensionGalleryManifest: IExtensionGalleryManifest | null): Promise { CONTEXT_GALLERY_SORT_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.sorting?.map(s => s.name)?.join('_')}_UpdateDate_`); CONTEXT_GALLERY_FILTER_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.filtering?.map(s => s.name)?.join('_')}_`); - CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allRepositorySigned); + CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allPublicRepositorySigned); + CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allPrivateRepositorySigned); CONTEXT_GALLERY_HAS_EXTENSION_LINK.bindTo(this.contextKeyService).set(!!(extensionGalleryManifest && getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionDetailsViewUri))); } @@ -1572,7 +1513,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned'), CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned'), + ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED, ContextKeyExpr.not('extensionIsPrivate')), ContextKeyExpr.and(CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED, ContextKeyExpr.has('extensionIsPrivate')))), order: 1 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1580,7 +1522,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { - const action = instantiationService.createInstance(InstallAction, { installPreReleaseVersion: this.extensionsWorkbenchService.preferPreReleases }); + const action = instantiationService.createInstance(InstallAction, { installPreReleaseVersion: this.extensionManagementService.preferPreReleases }); action.extension = extension; return action.run(); } @@ -1602,7 +1544,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { const action = instantiationService.createInstance(InstallAction, { - installPreReleaseVersion: this.extensionsWorkbenchService.preferPreReleases, + installPreReleaseVersion: this.extensionManagementService.preferPreReleases, isMachineScoped: true, }); action.extension = extension; @@ -2045,6 +1987,21 @@ class TrustedPublishersInitializer implements IWorkbenchContribution { } } +class ExtensionToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'extensions.chat.toolsContribution'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + const searchExtensionsTool = instantiationService.createInstance(SearchExtensionsTool); + this._register(toolsService.registerToolData(SearchExtensionsToolData)); + this._register(toolsService.registerToolImplementation(SearchExtensionsToolData.id, searchExtensionsTool)); + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually); @@ -2061,6 +2018,8 @@ if (isWeb) { workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } +registerWorkbenchContribution2(ExtensionToolsContribution.ID, ExtensionToolsContribution, WorkbenchPhase.AfterRestored); + // Running Extensions registerAction2(ShowRuntimeExtensionsAction); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 572bb2605fe..862d00b9fd8 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; -import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService, shouldRequireRepositorySignatureFor } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { areSameExtensions, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -73,6 +73,7 @@ import { IUpdateService } from '../../../../platform/update/common/update.js'; import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js'; import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { IWorkbenchIssueService } from '../../issue/common/issue.js'; export class PromptExtensionInstallFailureAction extends Action { @@ -92,6 +93,7 @@ export class PromptExtensionInstallFailureAction extends Action { @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService, ) { super('extension.promptExtensionInstallFailure'); } @@ -158,7 +160,7 @@ export class PromptExtensionInstallFailureAction extends Action { return; } - if (ExtensionManagementErrorCode.SignatureVerificationFailed === (this.error.name) || ExtensionManagementErrorCode.SignatureVerificationInternal === (this.error.name)) { + if (ExtensionManagementErrorCode.SignatureVerificationFailed === (this.error.name)) { await this.dialogService.prompt({ type: 'error', message: localize('verification failed', "Cannot install '{0}' extension because {1} cannot verify the extension signature", this.extension.displayName, this.productService.nameLong), @@ -179,6 +181,33 @@ export class PromptExtensionInstallFailureAction extends Action { return; } + if (ExtensionManagementErrorCode.SignatureVerificationInternal === (this.error.name)) { + await this.dialogService.prompt({ + type: 'error', + message: localize('verification failed', "Cannot install '{0}' extension because {1} cannot verify the extension signature", this.extension.displayName, this.productService.nameLong), + detail: getErrorMessage(this.error), + buttons: [{ + label: localize('learn more', "Learn More"), + run: () => this.openerService.open('https://code.visualstudio.com/docs/editor/extension-marketplace#_the-extension-signature-cannot-be-verified-by-vs-code') + }, { + label: localize('report issue', "Report Issue"), + run: () => this.workbenchIssueService.openReporter({ + issueTitle: localize('report issue title', "Extension Signature Verification Failed: {0}", this.extension.displayName), + issueBody: localize('report issue body', "Please include following log `F1 > Open View... > Shared` below.\n\n") + }) + }, { + label: localize('install donot verify', "Install Anyway (Don't Verify Signature)"), + run: () => { + const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, }); + installAction.extension = this.extension; + return installAction.run(); + } + }], + cancelButton: true + }); + return; + } + const operationMessage = this.installOperation === InstallOperation.Update ? localize('update operation', "Error while updating '{0}' extension.", this.extension.displayName || this.extension.identifier.id) : localize('install operation', "Error while installing '{0}' extension.", this.extension.displayName || this.extension.identifier.id); let additionalMessage; @@ -470,7 +499,7 @@ export class InstallAction extends ExtensionAction { return; } - if (this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (this.extension.gallery && !this.extension.gallery.isSigned && shouldRequireRepositorySignatureFor(this.extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { const { result } = await this.dialogService.prompt({ type: Severity.Warning, message: localize('not signed', "'{0}' is an extension from an unknown source. Are you sure you want to install?", this.extension.displayName), @@ -665,12 +694,12 @@ export class InstallDropdownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, ) { super(`extensions.installActions`, InstallAction.CLASS, [ [ - instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionsWorkbenchService.preferPreReleases }), - instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionsWorkbenchService.preferPreReleases }), + instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionManagementService.preferPreReleases }), + instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionManagementService.preferPreReleases }), ] ]); } @@ -999,17 +1028,19 @@ export class UpdateAction extends ExtensionAction { } } - alert(localize('updateExtensionStart', "Updating extension {0} to version {1} started.", this.extension.displayName, this.extension.latestVersion)); - return this.install(this.extension); - } - - private async install(extension: IExtension): Promise { - const options = extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined; + const installOptions: InstallOptions = {}; + if (this.extension.local?.source === 'vsix' && this.extension.local.pinned) { + installOptions.pinned = false; + } + if (this.extension.local?.preRelease) { + installOptions.installPreReleaseVersion = true; + } try { - await this.extensionsWorkbenchService.install(extension, options); - alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", extension.displayName, extension.latestVersion)); + alert(localize('updateExtensionStart', "Updating extension {0} to version {1} started.", this.extension.displayName, this.extension.latestVersion)); + await this.extensionsWorkbenchService.install(this.extension, installOptions); + alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", this.extension.displayName, this.extension.latestVersion)); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, options, extension.latestVersion, InstallOperation.Update, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, installOptions, this.extension.latestVersion, InstallOperation.Update, err).run(); } } } @@ -1264,6 +1295,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) === true]); cksOverlay.push(['isPreReleaseExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName, prerelease: true }) === true]); cksOverlay.push(['extensionIsUnsigned', extension.gallery && !extension.gallery.isSigned]); + cksOverlay.push(['extensionIsPrivate', extension.gallery?.private]); const [colorThemes, fileIconThemes, productIconThemes, extensionUsesAuth] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes(), authenticationUsageService.extensionUsesAuth(extension.identifier.id.toLowerCase())]); cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]); @@ -2552,7 +2584,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned && shouldRequireRepositorySignatureFor(this.extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('not signed tooltip', "This extension is not signed by the Extension Marketplace.")) }, true); return; } @@ -2578,7 +2610,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.outdated && this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension)) { + if (this.extension.outdated) { const message = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension); if (message) { const markdown = new MarkdownString(); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 82f9f53d501..f327b08b0d5 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,17 +5,17 @@ import * as dom from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Action } from '../../../../base/common/actions.js'; -import { IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; +import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; +import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IListService, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; +import { IListService, IWorkbenchPagedListOptions, WorkbenchAsyncDataTree, WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; -import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Delegate, Renderer } from './extensionsList.js'; @@ -27,6 +27,125 @@ import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js'; import { getAriaLabelForExtension } from './extensionsViews.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; +import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { ExtensionAction, getContextMenuActions, ManageExtensionAction } from './extensionsActions.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js'; +import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js'; + +export class ExtensionsList extends Disposable { + + readonly list: WorkbenchPagedList; + private readonly contextMenuActionRunner = this._register(new ActionRunner()); + + constructor( + parent: HTMLElement, + viewId: string, + options: Partial>, + extensionsViewState: IExtensionsViewState, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @INotificationService notificationService: INotificationService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error))); + const delegate = new Delegate(); + const renderer = instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = viewDescriptorService.getViewLocationById(viewId); + if (viewLocation === ViewContainerLocation.Sidebar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); + this.list = instantiationService.createInstance(WorkbenchPagedList, `${viewId}-Extensions`, parent, delegate, [renderer], { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(extension: IExtension | null): string { + return getAriaLabelForExtension(extension); + }, + getWidgetAriaLabel(): string { + return localize('extensions', "Extensions"); + } + }, + overrideStyles: getLocationBasedViewColors(viewDescriptorService.getViewLocationById(viewId)).listOverrideStyles, + openOnSingleClick: true, + ...options + }) as WorkbenchPagedList; + this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); + this._register(this.list); + + this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { + this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); + })); + } + + setModel(model: IPagedModel) { + this.list.model = new DelayedPagedModel(model); + } + + layout(height?: number, width?: number): void { + this.list.layout(height, width); + } + + private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { + extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; + this.extensionsWorkbenchService.open(extension, options); + } + + private async onContextMenu(e: IListContextMenuEvent): Promise { + if (e.element) { + const disposables = new DisposableStore(); + const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); + const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element + : e.element; + manageExtensionAction.extension = extension; + let groups: IAction[][] = []; + if (manageExtensionAction.enabled) { + groups = await manageExtensionAction.getActionGroups(); + } else if (extension) { + groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof ExtensionAction) { + extensionAction.extension = extension; + } + })); + } + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + actionRunner: this.contextMenuActionRunner, + onHide: () => disposables.dispose() + }); + } + } +} export class ExtensionsGridView extends Disposable { diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 7f02128f2d3..d7c6d3c70be 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -34,7 +34,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { INotificationService, NotificationPriority } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, IPromptChoice, NotificationPriority } from '../../../../platform/notification/common/notification.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; @@ -68,6 +68,7 @@ import { KeyCode } from '../../../../base/common/keyCodes.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { URI } from '../../../../base/common/uri.js'; export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); @@ -986,6 +987,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { @IHostService private readonly hostService: IHostService, @ILogService private readonly logService: ILogService, @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService, ) { this.loopCheckForMaliciousExtensions(); } @@ -998,30 +1000,42 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { private async checkForMaliciousExtensions(): Promise { try { - const maliciousExtensions: ILocalExtension[] = []; + const maliciousExtensions: [ILocalExtension, string | undefined][] = []; let shouldRestartExtensions = false; let shouldReloadWindow = false; for (const extension of this.extensionsWorkbenchService.installed) { if (extension.isMalicious && extension.local) { - maliciousExtensions.push(extension.local); + maliciousExtensions.push([extension.local, extension.maliciousInfoLink]); shouldRestartExtensions = shouldRestartExtensions || extension.runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions; shouldReloadWindow = shouldReloadWindow || extension.runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow; } } if (maliciousExtensions.length) { - await this.extensionsManagementService.uninstallExtensions(maliciousExtensions.map(e => ({ extension: e, options: { remove: true } }))); - this.notificationService.prompt( - Severity.Warning, - localize('malicious warning', "The following extensions were found to be problematic and have been uninstalled: {0}", maliciousExtensions.map(e => e.identifier.id).join(', ')), - shouldRestartExtensions || shouldReloadWindow ? [{ - label: shouldRestartExtensions ? localize('restartNow', "Restart Extensions") : localize('reloadNow', "Reload Now"), - run: () => shouldRestartExtensions ? this.extensionsWorkbenchService.updateRunningExtensions() : this.hostService.reload() - }] : [], - { - sticky: true, - priority: NotificationPriority.URGENT + await this.extensionsManagementService.uninstallExtensions(maliciousExtensions.map(e => ({ extension: e[0], options: { remove: true } }))); + for (const [extension, link] of maliciousExtensions) { + const buttons: IPromptChoice[] = []; + if (shouldRestartExtensions || shouldReloadWindow) { + buttons.push({ + label: shouldRestartExtensions ? localize('restartNow', "Restart Extensions") : localize('reloadNow', "Reload Now"), + run: () => shouldRestartExtensions ? this.extensionsWorkbenchService.updateRunningExtensions() : this.hostService.reload() + }); + } + if (link) { + buttons.push({ + label: localize('learnMore', "Learn More"), + run: () => this.commandService.executeCommand('vscode.open', URI.parse(link)) + }); } - ); + this.notificationService.prompt( + Severity.Warning, + localize('malicious warning', "The extension '{0}' was found to be problematic and has been uninstalled", extension.manifest.displayName || extension.identifier.id), + buttons, + { + sticky: true, + priority: NotificationPriority.URGENT + } + ); + } } } catch (err) { diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index dd378644f9c..f6ece8cd747 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { isCancellationError, getErrorMessage, CancellationError } from '../../../../base/common/errors.js'; -import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { PagedModel, IPagedModel, DelayedPagedModel, IPager } from '../../../../base/common/paging.js'; import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -17,7 +16,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Delegate, Renderer } from './extensionsList.js'; import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js'; import { Query } from '../common/extensionQuery.js'; import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js'; @@ -25,7 +23,6 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; -import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from './extensionsActions.js'; import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; @@ -33,23 +30,19 @@ import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from '../../../browse import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { coalesce, distinct, range } from '../../../../base/common/arrays.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { IAction, Action, Separator, ActionRunner } from '../../../../base/common/actions.js'; +import { ActionRunner } from '../../../../base/common/actions.js'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IViewDescriptorService } from '../../../common/views.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; -import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { isOfflineError } from '../../../../base/parts/request/common/request.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; @@ -59,6 +52,7 @@ import { URI } from '../../../../base/common/uri.js'; import { isString } from '../../../../base/common/types.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { ExtensionsList } from './extensionsViewer.js'; export const NONE_CATEGORY = 'none'; @@ -156,11 +150,9 @@ export class ExtensionsListView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService @@ -196,46 +188,10 @@ export class ExtensionsListView extends ViewPane { const messageSeverityIcon = append(messageContainer, $('')); const messageBox = append(messageContainer, $('.message')); const extensionsList = append(container, $('.extensions-list')); - const delegate = new Delegate(); - this.extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, this.extensionsViewState, { - hoverOptions: { - position: () => { - const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); - if (viewLocation === ViewContainerLocation.Sidebar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; - } - if (viewLocation === ViewContainerLocation.AuxiliaryBar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; - } - return HoverPosition.RIGHT; - } - } - }); - this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { - multipleSelectionSupport: false, - setRowLineHeight: false, - horizontalScrolling: false, - accessibilityProvider: { - getAriaLabel(extension: IExtension | null): string { - return getAriaLabelForExtension(extension); - }, - getWidgetAriaLabel(): string { - return localize('extensions', "Extensions"); - } - }, - overrideStyles: this.getLocationBasedColors().listOverrideStyles, - openOnSingleClick: true - }) as WorkbenchPagedList; + this.extensionsViewState = this._register(new ExtensionsViewState()); + this.list = this._register(this.instantiationService.createInstance(ExtensionsList, extensionsList, this.id, {}, this.extensionsViewState)).list; ExtensionResultsListFocused.bindTo(this.list.contextKeyService); - this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this)); - this._register(this.list); - this._register(this.extensionsViewState); - - this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); - })); this.bodyTemplate = { extensionsList, @@ -327,44 +283,6 @@ export class ExtensionsListView extends ViewPane { return Promise.resolve(emptyModel); } - private async onContextMenu(e: IListContextMenuEvent): Promise { - if (e.element) { - const disposables = new DisposableStore(); - const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); - const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element - : e.element; - manageExtensionAction.extension = extension; - let groups: IAction[][] = []; - if (manageExtensionAction.enabled) { - groups = await manageExtensionAction.getActionGroups(); - } else if (extension) { - groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); - groups.forEach(group => group.forEach(extensionAction => { - if (extensionAction instanceof ExtensionAction) { - extensionAction.extension = extension; - } - })); - } - const actions: IAction[] = []; - for (const menuActions of groups) { - for (const menuAction of menuActions) { - actions.push(menuAction); - if (isDisposable(menuAction)) { - disposables.add(menuAction); - } - } - actions.push(new Separator()); - } - actions.pop(); - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions, - actionRunner: this.contextMenuActionRunner, - onHide: () => disposables.dispose() - }); - } - } - private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise { const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; const ids: string[] = []; @@ -1194,30 +1112,6 @@ export class ExtensionsListView extends ViewPane { } } - private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { - extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; - this.extensionsWorkbenchService.open(extension, options).then(undefined, err => this.onError(err)); - } - - private onError(err: any): void { - if (isCancellationError(err)) { - return; - } - - const message = err && err.message || ''; - - if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ]); - - this.notificationService.error(error); - return; - } - - this.notificationService.error(err); - } - override dispose(): void { super.dispose(); if (this.queryRequest) { @@ -1454,11 +1348,9 @@ export class StaticQueryExtensionsView extends ExtensionsListView { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService preferencesService: IPreferencesService, @IStorageService storageService: IStorageService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService logService: ILogService @@ -1466,7 +1358,7 @@ export class StaticQueryExtensionsView extends ExtensionsListView { super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService, extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService, - preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, + storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService, uriIdentityService, logService); } diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 7a2dd56b319..b0cf41cffd3 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -275,7 +275,7 @@ export class PublisherWidget extends ExtensionWidget { append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon)); if (this.small) { - if (this.extension.publisherDomain) { + if (this.extension.publisherDomain?.verified) { append(this.element, verifiedPublisher); } append(this.element, publisherDisplayName); @@ -287,7 +287,7 @@ export class PublisherWidget extends ExtensionWidget { this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName))); append(this.element, publisherDisplayName); - if (this.extension.publisherDomain) { + if (this.extension.publisherDomain?.verified) { append(this.element, verifiedPublisher); const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); verifiedPublisher.tabIndex = 0; diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index bcf99d677c2..8ee497e156a 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -23,10 +23,12 @@ import { AllowedExtensionsConfigKey, EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, ExtensionManagementError, - ExtensionManagementErrorCode + ExtensionManagementErrorCode, + MaliciousExtensionInfo, + shouldRequireRepositorySignatureFor } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, findMatchingMaliciousEntry } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -215,7 +217,7 @@ export class Extension implements IExtension { } get private(): boolean { - return this.local ? this.local.private : this.gallery ? this.gallery.private : false; + return this.gallery ? this.gallery.private : this.local ? this.local.private : false; } get pinned(): boolean { @@ -294,9 +296,13 @@ export class Extension implements IExtension { return this.stateProvider(this); } - private malicious: boolean = false; - public get isMalicious(): boolean { - return this.malicious || this.enablementState === EnablementState.DisabledByMalicious; + private malicious: MaliciousExtensionInfo | undefined; + public get isMalicious(): boolean | undefined { + return !!this.malicious || this.enablementState === EnablementState.DisabledByMalicious; + } + + public get maliciousInfoLink(): string | undefined { + return this.malicious?.learnMoreLink; } public deprecationInfo: IDeprecationInfo | undefined; @@ -380,9 +386,8 @@ export class Extension implements IExtension { return !!this.gallery?.properties.isPreReleaseVersion; } - private _extensionEnabledWithPreRelease: boolean | undefined; get hasPreReleaseVersion(): boolean { - return !!this.gallery?.hasPreReleaseVersion || !!this.local?.hasPreReleaseVersion || !!this._extensionEnabledWithPreRelease; + return this.gallery ? this.gallery.hasPreReleaseVersion : !!this.local?.hasPreReleaseVersion; } get hasReleaseVersion(): boolean { @@ -556,9 +561,8 @@ ${this.description} } setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void { - this.malicious = isMalicious(this.identifier, extensionsControlManifest.malicious); + this.malicious = findMatchingMaliciousEntry(this.identifier, extensionsControlManifest.malicious); this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined; - this._extensionEnabledWithPreRelease = extensionsControlManifest?.extensionsEnabledWithPreRelease?.includes(this.identifier.id.toLowerCase()); } private getManifestFromLocalOrResource(): IExtensionManifest | null { @@ -659,7 +663,7 @@ class Extensions extends Disposable { const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist - if (extension.local && extension.local.identifier.uuid !== gallery.identifier.uuid) { + if (extension.local && !extension.local.identifier.uuid) { extension.local = await this.updateMetadata(extension.local, gallery); } if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { @@ -933,8 +937,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onReset = new Emitter(); get onReset() { return this._onReset.event; } - readonly preferPreReleases = this.productService.quality !== 'stable'; - private installing: IExtension[] = []; private tasksInProgress: CancelablePromise[] = []; @@ -978,10 +980,7 @@ 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.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, @@ -1313,7 +1312,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1; const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2; options.text = options.text ? this.resolveQueryText(options.text) : options.text; - options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease; + options.includePreRelease = isUndefined(options.includePreRelease) ? this.extensionManagementService.preferPreReleases : options.includePreRelease; const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); const pager = await this.galleryService.query(options, token); @@ -1337,7 +1336,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return []; } - extensionInfos.forEach(e => e.preRelease = e.preRelease ?? this.preferPreReleases); + extensionInfos.forEach(e => e.preRelease = e.preRelease ?? this.extensionManagementService.preferPreReleases); const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, arg1, arg2); this.syncInstalledExtensionsWithGallery(galleryExtensions); @@ -1829,7 +1828,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async checkForUpdates(reason?: string, onlyBuiltin?: boolean): Promise { if (reason) { - this.logService.info(`[Extensions]: Checking for updates. Reason: ${reason}`); + this.logService.trace(`[Extensions]: Checking for updates. Reason: ${reason}`); } else { this.logService.trace(`[Extensions]: Checking for updates`); } @@ -2023,27 +2022,34 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async autoUpdateExtensions(): Promise { const toUpdate: IExtension[] = []; + const disabledAutoUpdate = []; + const consentRequired = []; for (const extension of this.outdated) { if (!this.shouldAutoUpdateExtension(extension)) { - this.logService.info('Auto update disabled for extension', extension.identifier.id); + disabledAutoUpdate.push(extension.identifier.id); continue; } if (await this.shouldRequireConsentToUpdate(extension)) { - this.logService.info('Auto update consent required for extension', extension.identifier.id); + consentRequired.push(extension.identifier.id); continue; } toUpdate.push(extension); } + if (disabledAutoUpdate.length) { + this.logService.trace('Auto update disabled for extensions', disabledAutoUpdate.join(', ')); + } + + if (consentRequired.length) { + this.logService.info('Auto update consent required for extensions', consentRequired.join(', ')); + } + if (!toUpdate.length) { return; } const productVersion = this.getProductVersion(); - await Promises.settled(toUpdate.map(e => { - this.logService.info('Auto updating extension', e.identifier.id); - return this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }); - })); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); } private getProductVersion(): IProductVersion { @@ -2113,11 +2119,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return; } - if (extension.local?.manifest.main || extension.local?.manifest.browser) { + if (!extension.gallery || !extension.local) { return; } - if (!extension.gallery) { + if (extension.local.identifier.uuid && extension.local.identifier.uuid !== extension.gallery.identifier.uuid) { + return nls.localize('consentRequiredToUpdateRepublishedExtension', "The marketplace metadata of this extension changed, likely due to a re-publish."); + } + + if (!extension.local.manifest.engines.vscode || extension.local.manifest.main || extension.local.manifest.browser) { return; } @@ -2298,7 +2308,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (extension.gallery) { - if (!extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (!extension.gallery.isSigned && shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { return new MarkdownString().appendText(nls.localize('not signed', "This extension is not signed.")); } @@ -2342,7 +2352,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isString(arg)) { extension = this.local.find(e => areSameExtensions(e.identifier, { id: arg })); if (!extension?.isBuiltin) { - installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.preferPreReleases }; + installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.extensionManagementService.preferPreReleases }; } } // Install by gallery @@ -2404,7 +2414,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!gallery) { const id = isString(arg) ? arg : (arg).identifier.id; const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest(); - const reportIssueUri = manifest ? getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ReportIssueUri) : undefined; + const reportIssueUri = manifest ? getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ContactSupportUri) : undefined; const reportIssueMessage = reportIssueUri ? nls.localize('report issue', "If this issue persists, please report it at {0}", reportIssueUri.toString()) : ''; if (installOptions.version) { const message = nls.localize('not found version', "The extension '{0}' cannot be installed because the requested version '{1}' was not found.", id, installOptions.version); @@ -2614,7 +2624,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.withProgress({ location: ProgressLocation.Extensions, - title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), + title: nls.localize('uninstallingExtension', 'Uninstalling extension...'), source: `${extension.identifier.id}` }, () => this.extensionManagementService.uninstallExtensions(extensionsToUninstall).then(() => undefined)); } @@ -2688,7 +2698,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension await Promise.all(this.getAllExtensions().map(async extensions => { const local = extensions.local.find(e => areSameExtensions(e.identifier, extension.identifier))?.local; if (local && local.isApplicationScoped === isApplicationScoped) { - await this.extensionManagementService.toggleAppliationScope(local, this.userDataProfileService.currentProfile.extensionsResource); + await this.extensionManagementService.toggleApplicationScope(local, this.userDataProfileService.currentProfile.extensionsResource); } })); } @@ -2729,7 +2739,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation | string): Promise { - const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); + const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension...", extension.displayName) : nls.localize('installing extension', 'Installing extension...'); return this.withProgress({ location: progressLocation ?? ProgressLocation.Extensions, title @@ -2757,7 +2767,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (existingExtension) { installOptions = installOptions || {}; if (existingExtension.latestVersion === manifest.version) { - installOptions.pinned = existingExtension.local?.pinned || !this.shouldAutoUpdateExtension(existingExtension); + installOptions.pinned = installOptions.pinned ?? (existingExtension.local?.pinned || !this.shouldAutoUpdateExtension(existingExtension)); } else { installOptions.installGivenVersion = true; } @@ -2767,7 +2777,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions: InstallExtensionOptions, servers: IExtensionManagementServer[] | undefined): Promise { installOptions = installOptions ?? {}; - installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); + installOptions.pinned = installOptions.pinned ?? (extension.local?.pinned || !this.shouldAutoUpdateExtension(extension)); if (extension.local && !servers) { installOptions.productVersion = this.getProductVersion(); installOptions.operation = InstallOperation.Update; diff --git a/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index 6d98e545b6e..927e707f50f 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -305,7 +305,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { const promptedRecommendations = language !== PLAINTEXT_LANGUAGE_ID ? this.getPromptedRecommendations()[language] : undefined; if (promptedRecommendations) { - recommendations = recommendations.filter(extensionId => promptedRecommendations.includes(extensionId)); + recommendations = recommendations.filter(extensionId => !promptedRecommendations.includes(extensionId)); } if (recommendations.length === 0) { diff --git a/code/src/vs/workbench/contrib/extensions/common/extensions.ts b/code/src/vs/workbench/contrib/extensions/common/extensions.ts index ce233779e20..ff3b6412d43 100644 --- a/code/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/code/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -104,7 +104,8 @@ export interface IExtension { readonly local?: ILocalExtension; gallery?: IGalleryExtension; readonly resourceExtension?: IResourceExtension; - readonly isMalicious: boolean; + readonly isMalicious: boolean | undefined; + readonly maliciousInfoLink: string | undefined; readonly deprecationInfo?: IDeprecationInfo; } @@ -128,7 +129,6 @@ export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; readonly onReset: Event; - readonly preferPreReleases: boolean; readonly local: IExtension[]; readonly installed: IExtension[]; readonly outdated: IExtension[]; diff --git a/code/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/code/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts new file mode 100644 index 00000000000..fc557068ed8 --- /dev/null +++ b/code/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; + +export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; + +export const SearchExtensionsToolData: IToolData = { + id: SearchExtensionsToolId, + toolReferenceName: 'extensions', + canBeReferencedInPrompt: true, + icon: ThemeIcon.fromId(Codicon.extensions.id), + supportsToolPicker: true, + displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), + modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), + source: { type: 'internal' }, + inputSchema: { + type: 'object', + properties: { + category: { + type: 'string', + description: 'The category of extensions to search for', + enum: EXTENSION_CATEGORIES, + }, + keywords: { + type: 'array', + items: { + type: 'string', + }, + description: 'The keywords to search for', + }, + ids: { + type: 'array', + items: { + type: 'string', + }, + description: 'The ids of the extensions to search for', + }, + }, + } +}; + +type InputParams = { + category?: string; + keywords?: string; + ids?: string[]; +}; + +type ExtensionData = { + id: string; + name: string; + description: string; + installed: boolean; + installCount: number; + rating: number; + categories: readonly string[]; + tags: readonly string[]; +}; + +export class SearchExtensionsTool implements IToolImpl { + + constructor( + @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, + ) { } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { + const params = invocation.parameters as InputParams; + if (!params.keywords?.length && !params.category && !params.ids?.length) { + return { + content: [{ + kind: 'text', + value: localize('searchExtensionsTool.noInput', 'Please provide a category or keywords or ids to search for.') + }] + }; + } + + const extensionsMap = new Map(); + + const addExtension = (extensions: IExtension[]) => { + for (const extension of extensions) { + if (extension.deprecationInfo || extension.isMalicious) { + continue; + } + extensionsMap.set(extension.identifier.id.toLowerCase(), { + id: extension.identifier.id, + name: extension.displayName, + description: extension.description, + installed: extension.state === ExtensionState.Installed, + installCount: extension.installCount ?? 0, + rating: extension.rating ?? 0, + categories: extension.categories ?? [], + tags: extension.gallery?.tags ?? [] + }); + } + }; + + const queryAndAddExtensions = async (text: string) => { + const extensions = await this.extensionWorkbenchService.queryGallery({ + text, + pageSize: 10, + sortBy: SortBy.InstallCount + }, token); + if (extensions.firstPage.length) { + addExtension(extensions.firstPage); + } + }; + + // Search for extensions by their ids + if (params.ids?.length) { + const extensions = await this.extensionWorkbenchService.getExtensions(params.ids.map(id => ({ id })), token); + addExtension(extensions); + } + + if (params.keywords?.length) { + for (const keyword of params.keywords ?? []) { + if (keyword === 'featured') { + await queryAndAddExtensions('featured'); + } else { + let text = params.category ? `category:"${params.category}"` : ''; + text = keyword ? `${text} ${keyword}`.trim() : text; + await queryAndAddExtensions(text); + } + } + } else { + await queryAndAddExtensions(`category:"${params.category}"`); + } + + const result = Array.from(extensionsMap.values()); + + return { + content: [{ + kind: 'text', + value: `Here are the list of extensions:\n${JSON.stringify(result)}\n. Important: Use the following format to display extensions to the user because there is a renderer available to parse these extensions in this format and display them with all details. So, do not describe about the extensions to the user.\n\`\`\`vscode-extensions\nextensionId1,extensionId2\n\`\`\`\n.` + }], + toolResultDetails: { + input: JSON.stringify(params), + output: [{ type: 'text', value: JSON.stringify(result.map(extension => extension.id)) }] + } + }; + } +} + diff --git a/code/src/vs/workbench/contrib/files/browser/explorerFileContrib.ts b/code/src/vs/workbench/contrib/files/browser/explorerFileContrib.ts index 116ec0994a9..4eb1ae4b3d3 100644 --- a/code/src/vs/workbench/contrib/files/browser/explorerFileContrib.ts +++ b/code/src/vs/workbench/contrib/files/browser/explorerFileContrib.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -36,8 +36,8 @@ export interface IExplorerFileContributionRegistry { register(descriptor: IExplorerFileContributionDescriptor): void; } -class ExplorerFileContributionRegistry implements IExplorerFileContributionRegistry { - private readonly _onDidRegisterDescriptor = new Emitter(); +class ExplorerFileContributionRegistry extends Disposable implements IExplorerFileContributionRegistry { + private readonly _onDidRegisterDescriptor = this._register(new Emitter()); public readonly onDidRegisterDescriptor = this._onDidRegisterDescriptor.event; private readonly descriptors: IExplorerFileContributionDescriptor[] = []; diff --git a/code/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/code/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index ab3d1dabf53..2bed6276108 100644 --- a/code/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/code/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -174,7 +174,7 @@ registerAction2(class StartReadHints extends EditorAction2 { constructor() { super({ id: 'inlayHints.startReadingLineWithHint', - title: localize2('read.title', "Read Line with Inline Hints"), + title: localize2('read.title', "Read Line with Inlay Hints"), precondition: EditorContextKeys.hasInlayHintsProvider, f1: true }); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index c26b4b49211..ab86b66a756 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,7 +7,7 @@ import { EditorContributionInstantiation, registerEditorContribution } from '../ import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController, InlineChatController1, InlineChatController2 } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -28,8 +28,11 @@ registerEditorContribution(InlineChatController2.ID, InlineChatController2, Edit registerEditorContribution(INLINE_CHAT_ID, InlineChatController1, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerAction2(InlineChatActions.StopSessionAction2); +registerAction2(InlineChatActions.KeepSessionAction2); +registerAction2(InlineChatActions.UndoSessionAction2); +registerAction2(InlineChatActions.CloseSessionAction2); registerAction2(InlineChatActions.RevealWidget); +registerAction2(InlineChatActions.CancelRequestAction); // --- browser @@ -53,7 +56,8 @@ const editActionMenuItem: IMenuItem = { when: ContextKeyExpr.and( ChatContextKeys.inputHasText, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_EDITING + CTX_INLINE_CHAT_EDITING, + CTX_INLINE_CHAT_HAS_AGENT ), }; @@ -67,7 +71,8 @@ const generateActionMenuItem: IMenuItem = { when: ContextKeyExpr.and( ChatContextKeys.inputHasText, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_EDITING.toNegated() + CTX_INLINE_CHAT_EDITING.toNegated(), + CTX_INLINE_CHAT_HAS_AGENT ), }; diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index dc31d83bea9..fbf934dcd16 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -12,7 +12,7 @@ import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/code import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatController1, InlineChatController2, InlineChatRunOptions } from './inlineChatController.js'; import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_HAS_AGENT2, MENU_INLINE_CHAT_SIDE } from '../common/inlineChat.js'; -import { ctxIsGlobalEditingSession, ctxRequestCount } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; +import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession, ctxRequestCount } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -178,6 +178,22 @@ export abstract class AbstractInline1ChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); constructor(desc: IAction2Options) { + + const massageMenu = (menu: IAction2Options['menu'] | undefined) => { + if (Array.isArray(menu)) { + for (const entry of menu) { + entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, entry.when); + } + } else if (menu) { + menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, menu.when); + } + }; + if (Array.isArray(desc.menu)) { + massageMenu(desc.menu); + } else { + massageMenu(desc.menu); + } + super({ ...desc, category: AbstractInline1ChatAction.category, @@ -385,9 +401,11 @@ export class CloseAction extends AbstractInline1ChatAction { id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - ), + when: CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() + }, { + id: MENU_INLINE_CHAT_SIDE, + group: 'navigation', + when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.None) }] }); } @@ -530,6 +548,21 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); constructor(desc: IAction2Options) { + const massageMenu = (menu: IAction2Options['menu'] | undefined) => { + if (Array.isArray(menu)) { + for (const entry of menu) { + entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, entry.when); + } + } else if (menu) { + menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, menu.when); + } + }; + if (Array.isArray(desc.menu)) { + massageMenu(desc.menu); + } else { + massageMenu(desc.menu); + } + super({ ...desc, category: AbstractInline2ChatAction.category, @@ -576,15 +609,75 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void; } -export class StopSessionAction2 extends AbstractInline2ChatAction { +class KeepOrUndoSessionAction extends AbstractInline2ChatAction { + + constructor(id: string, private readonly _keep: boolean) { + super({ + id, + title: _keep + ? localize2('Keep', "Keep") + : localize2('Undo', "Undo"), + f1: true, + icon: _keep ? Codicon.check : Codicon.discard, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ctxHasRequestInProgress.negate()), + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: _keep + ? KeyMod.CtrlCmd | KeyCode.Enter + : KeyMod.CtrlCmd | KeyCode.Backspace + }], + menu: [{ + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, ContextKeyExpr.greater(ctxRequestCount.key, 0), ctxHasEditorModification), + }] + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ..._args: any[]): Promise { + const inlineChatSessions = accessor.get(IInlineChatSessionService); + if (!editor.hasModel()) { + return; + } + const textModel = editor.getModel(); + const session = inlineChatSessions.getSession2(textModel.uri); + if (session) { + if (this._keep) { + await session.editingSession.accept(); + } else { + await session.editingSession.reject(); + } + session.dispose(); + } + } +} + +export class KeepSessionAction2 extends KeepOrUndoSessionAction { + constructor() { + super('inlineChat2.keep', true); + } +} + +export class UndoSessionAction2 extends KeepOrUndoSessionAction { + constructor() { + super('inlineChat2.undo', false); + } +} + +export class CloseSessionAction2 extends AbstractInline2ChatAction { constructor() { super({ - id: 'inlineChat2.stop', - title: localize2('stop', "Undo & Close"), + id: 'inlineChat2.close', + title: localize2('close2', "Close"), f1: true, icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, + precondition: ContextKeyExpr.and( + CTX_INLINE_CHAT_VISIBLE, + ctxHasRequestInProgress.negate(), + ContextKeyExpr.or(ctxRequestCount.isEqualTo(0), ctxHasEditorModification.negate()) + ), keybinding: [{ when: ctxRequestCount.isEqualTo(0), weight: KeybindingWeight.WorkbenchContrib, @@ -593,21 +686,25 @@ export class StopSessionAction2 extends AbstractInline2ChatAction { weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, }], - menu: { + menu: [{ id: MENU_INLINE_CHAT_SIDE, group: 'navigation', - when: CTX_INLINE_CHAT_HAS_AGENT2 - } + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, ctxRequestCount.isEqualTo(0)), + }, { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT2, ctxHasEditorModification.negate()), + }] }); } runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ...args: any[]): void { const inlineChatSessions = accessor.get(IInlineChatSessionService); - if (!editor.hasModel()) { - return; + if (editor.hasModel()) { + const textModel = editor.getModel(); + inlineChatSessions.getSession2(textModel.uri)?.dispose(); } - const textModel = editor.getModel(); - inlineChatSessions.getSession2(textModel.uri)?.dispose(); } } @@ -619,7 +716,9 @@ export class RevealWidget extends AbstractInline2ChatAction { f1: true, icon: Codicon.copilot, precondition: ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ContextKeyExpr.greaterEquals(ctxRequestCount.key, 1)), - toggled: CTX_INLINE_CHAT_VISIBLE, + toggled: { + condition: CTX_INLINE_CHAT_VISIBLE, + }, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI @@ -641,3 +740,33 @@ export class RevealWidget extends AbstractInline2ChatAction { ctrl.markActiveController(); } } + +export class CancelRequestAction extends AbstractInline2ChatAction { + constructor() { + super({ + id: 'inlineChat2.cancelRequest', + title: localize2('cancel', "Cancel Request"), + f1: true, + icon: Codicon.stopCircle, + precondition: ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress), + toggled: CTX_INLINE_CHAT_VISIBLE, + menu: { + id: MenuId.ChatEditingEditorContent, + when: ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress), + group: 'a_request', + order: 1, + } + }); + } + + runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, _editor: ICodeEditor): void { + const chatService = accessor.get(IChatService); + + const { viewModel } = ctrl.widget.chatWidget; + if (viewModel) { + ctrl.toggleWidgetUntilNextRequest(); + ctrl.markActiveController(); + chatService.cancelCurrentRequestForSession(viewModel.sessionId); + } + } +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 6ce57174001..194f4a239c0 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -11,11 +11,13 @@ import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; @@ -41,10 +43,10 @@ import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/edit import { IViewsService } from '../../../services/views/common/viewsService.js'; import { showChatView } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; -import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; +import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatRequestVariableEntry, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; @@ -53,7 +55,11 @@ 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 { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { resolveImageEditorAttachContext } from '../../chat/browser/chatAttachmentResolve.js'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -79,12 +85,13 @@ export abstract class InlineChatRunOptions { initialSelection?: ISelection; initialRange?: IRange; message?: string; + attachments?: URI[]; autoSend?: boolean; existingSession?: Session; position?: IPosition; static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { - const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; + const { initialSelection, initialRange, message, autoSend, position, existingSession, attachments: attachments } = options; if ( typeof message !== 'undefined' && typeof message !== 'string' || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' @@ -92,6 +99,7 @@ export abstract class InlineChatRunOptions { || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) || typeof position !== 'undefined' && !Position.isIPosition(position) || typeof existingSession !== 'undefined' && !(existingSession instanceof Session) + || typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI)) ) { return false; } @@ -111,10 +119,10 @@ export class InlineChatController implements IEditorContribution { constructor( editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, ) { - const inlineChat2 = observableFromEvent(this, Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(new Set(CTX_INLINE_CHAT_HAS_AGENT2.keys()))), () => contextKeyService.contextMatchesRules(CTX_INLINE_CHAT_HAS_AGENT2)); + const inlineChat2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configurationService); this._delegate = derived(r => { if (inlineChat2.read(r)) { @@ -199,6 +207,8 @@ export class InlineChatController1 implements IEditorContribution { @IChatService private readonly _chatService: IChatService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, + @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, + @IFileService private readonly _fileService: IFileService, ) { this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService); @@ -574,6 +584,12 @@ export class InlineChatController1 implements IEditorContribution { barrier.open(); })); + if (options.attachments) { + await Promise.all(options.attachments.map(async attachment => { + await this._ui.value.widget.chatWidget.attachmentModel.addFile(attachment); + })); + delete options.attachments; + } if (options.autoSend) { delete options.autoSend; this._showWidget(this._session.headless, false); @@ -1069,6 +1085,8 @@ export class InlineChatController1 implements IEditorContribution { // fully or partially applied edits someApplied = someApplied || Boolean(part.state?.applied); lastEdit = part; + part.edits = []; + part.state = undefined; } } } @@ -1078,7 +1096,6 @@ export class InlineChatController1 implements IEditorContribution { if (someApplied) { assertType(lastEdit); lastEdit.edits = [doEdits]; - lastEdit.state!.applied = 0; } await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel); @@ -1166,6 +1183,21 @@ export class InlineChatController1 implements IEditorContribution { get isActive() { return Boolean(this._currentRun); } + + async createImageAttachment(attachment: URI): Promise { + if (attachment.scheme === Schemas.file) { + if (await this._fileService.canHandleResource(attachment)) { + return await resolveImageEditorAttachContext(this._fileService, this._dialogService, attachment); + } + } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { + const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); + if (extractedImages) { + return await resolveImageEditorAttachContext(this._fileService, this._dialogService, attachment, extractedImages); + } + } + + return undefined; + } } export class InlineChatController2 implements IEditorContribution { @@ -1198,6 +1230,11 @@ export class InlineChatController2 implements IEditorContribution { @IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService, @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, + @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, + @IFileService private readonly _fileService: IFileService, + @IDialogService private readonly _dialogService: IDialogService, + @IEditorService private readonly _editorService: IEditorService, + @IInlineChatSessionService inlineChatService: IInlineChatSessionService, ) { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); @@ -1274,7 +1311,7 @@ export class InlineChatController2 implements IEditorContribution { break; } } - if (!foundOne && _editor.hasWidgetFocus()) { + if (!foundOne && editorObs.isFocused.read(r)) { this._isActiveController.set(true, undefined); } })); @@ -1295,40 +1332,44 @@ export class InlineChatController2 implements IEditorContribution { const { chatModel } = session; const showShowUntil = this._showWidgetOverrideObs.read(r); const hasNoRequests = chatModel.getRequests().length === 0; - + const hideOnRequest = inlineChatService.hideOnRequest.read(r); const responseListener = store.add(new MutableDisposable()); - store.add(chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - transaction(tx => { - this._showWidgetOverrideObs.set(false, tx); - visibleSessionObs.set(undefined, tx); - }); - const { response } = e.request; - if (!response) { - return; - } - responseListener.value = response.onDidChange(async e => { - - if (!response.isComplete) { + if (hideOnRequest) { + // hide the request once the request has been added, reveal it again when no edit was made + // or when an error happened + store.add(chatModel.onDidChange(e => { + if (e.kind === 'addRequest') { + transaction(tx => { + this._showWidgetOverrideObs.set(false, tx); + visibleSessionObs.set(undefined, tx); + }); + const { response } = e.request; + if (!response) { return; } + responseListener.value = response.onDidChange(async e => { + + if (!response.isComplete) { + return; + } + + const shouldShow = response.isCanceled // cancelled + || response.result?.errorDetails // errors + || !response.response.value.find(part => part.kind === 'textEditGroup' + && part.edits.length > 0 + && isEqual(part.uri, model.uri)); // NO edits for file + + if (shouldShow) { + visibleSessionObs.set(session, undefined); + } + }); + } + })); + } - const shouldShow = response.isCanceled // cancelled - || response.result?.errorDetails // errors - || !response.response.value.find(part => part.kind === 'textEditGroup' - && part.edits.length > 0 - && isEqual(part.uri, model.uri)); // NO edits for file - - if (shouldShow) { - visibleSessionObs.set(session, undefined); - } - }); - } - })); - - if (showShowUntil || hasNoRequests) { + if (showShowUntil || hasNoRequests || !hideOnRequest) { visibleSessionObs.set(session, undefined); } else { visibleSessionObs.set(undefined, undefined); @@ -1351,7 +1392,23 @@ export class InlineChatController2 implements IEditorContribution { } this._zone.value.reveal(this._zone.value.position!); this._zone.value.widget.focus(); - session.editingSession.getEntry(session.uri)?.autoAcceptController.get()?.cancel(); + this._zone.value.widget.updateToolbar(true); + const entry = session.editingSession.getEntry(session.uri); + + entry?.autoAcceptController.get()?.cancel(); + + const requestCount = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().length).read(r); + this._zone.value.widget.updateToolbar(requestCount > 0); + } + })); + + this._store.add(autorun(r => { + + const session = visibleSessionObs.read(r); + const entry = session?.editingSession.readEntry(session.uri, r); + const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor); + if (pane && entry) { + entry?.getEditorIntegration(pane); } })); } @@ -1393,6 +1450,12 @@ export class InlineChatController2 implements IEditorContribution { if (arg.initialSelection) { this._editor.setSelection(arg.initialSelection); } + if (arg.attachments) { + await Promise.all(arg.attachments.map(async attachment => { + await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); + })); + delete arg.attachments; + } if (arg.message) { this._zone.value.widget.chatWidget.setInput(arg.message); if (arg.autoSend) { @@ -1403,7 +1466,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; } @@ -1411,6 +1474,24 @@ export class InlineChatController2 implements IEditorContribution { const value = this._currentSession.get(); value?.editingSession.accept(); } + + async createImageAttachment(attachment: URI): Promise { + const value = this._currentSession.get(); + if (!value) { + return undefined; + } + if (attachment.scheme === Schemas.file) { + if (await this._fileService.canHandleResource(attachment)) { + return await resolveImageEditorAttachContext(this._fileService, this._dialogService, attachment); + } + } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { + const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); + if (extractedImages) { + return await resolveImageEditorAttachContext(this._fileService, this._dialogService, attachment, extractedImages); + } + } + return undefined; + } } export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken): Promise { @@ -1419,16 +1500,15 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito } const chatService = accessor.get(IChatService); - const chatEditingService = accessor.get(IChatEditingService); - const uri = editor.getModel().uri; const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false); - const editSession = await chatEditingService.createEditingSession(chatModel); + chatModel.startEditingSession(true); + + const editSession = await chatModel.editingSessionObs?.promise; const store = new DisposableStore(); store.add(chatModel); - store.add(editSession); // STREAM const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); @@ -1446,16 +1526,16 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); if (!token.isCancellationRequested) { - chatRequest.response.complete(); + chatModel.completeResponse(chatRequest); } const isSettled = derived(r => { - const entry = editSession.readEntry(uri, r); + const entry = editSession?.readEntry(uri, r); if (!entry) { 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/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index c17f655ba9f..656c54d713c 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -237,7 +237,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont return undefined; } - if (model.getLanguageId() === PLAINTEXT_LANGUAGE_ID || model.getLanguageId() === 'markdown') { + if (model.getLanguageId() === PLAINTEXT_LANGUAGE_ID || model.getLanguageId() === 'markdown' || model.getLanguageId() === 'search-result') { return undefined; } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index a6e06ad4b36..c20396091f0 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -5,6 +5,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; @@ -63,6 +64,8 @@ export interface IInlineChatSessionService { dispose(): void; + hideOnRequest: IObservable; + createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; getSession2(uri: URI): IInlineChatSession2 | undefined; onDidChangeSessions: Event; diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 97b36199564..2ebe866eed1 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -20,9 +20,11 @@ import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/m import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -30,10 +32,10 @@ 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'; +import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; @@ -74,6 +76,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { private readonly _sessions = new Map(); private readonly _keyComputers = new Map(); + readonly hideOnRequest: IObservable; + constructor( @ITelemetryService private readonly _telemetryService: ITelemetryService, @IModelService private readonly _modelService: IModelService, @@ -87,7 +91,14 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatService private readonly _chatService: IChatService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - ) { } + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + + const v2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, this._configurationService); + + this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService) + .map((value, r) => v2.read(r) && value); + } dispose() { this._store.dispose(); @@ -337,7 +348,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._onWillStartSession.fire(editor as IActiveCodeEditor); - const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token, false); + const chatModel = this._chatService.startSession(ChatAgentLocation.Panel, token, false); const editingSession = await chatModel.editingSessionObs?.promise!; const widget = this._chatWidgetService.getWidgetBySessionId(chatModel.sessionId); @@ -361,7 +372,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); }); @@ -414,27 +425,29 @@ export class InlineChatEnabler { @IContextKeyService contextKeyService: IContextKeyService, @IChatAgentService chatAgentService: IChatAgentService, @IEditorService editorService: IEditorService, + @IConfigurationService configService: IConfigurationService, ) { this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); - const updateAgent = () => { - const agent = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); - if (agent?.id === 'github.copilot.editor' || agent?.id === 'setup.editor') { - this._ctxHasProvider.set(true); + const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + const inlineChat2Obs = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configService); + + this._store.add(autorun(r => { + const v2 = inlineChat2Obs.read(r); + const agent = agentObs.read(r); + if (!agent) { + this._ctxHasProvider.reset(); this._ctxHasProvider2.reset(); - } else if (agent?.id === 'github.copilot.editingSessionEditor') { + } else if (v2) { this._ctxHasProvider.reset(); this._ctxHasProvider2.set(true); } else { - this._ctxHasProvider.reset(); + this._ctxHasProvider.set(true); this._ctxHasProvider2.reset(); } - }; - - this._store.add(chatAgentService.onDidChangeAgents(updateAgent)); - updateAgent(); + })); const updateEditor = () => { const ctrl = editorService.activeEditorPane?.getControl(); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index d3d0b420edf..6207826407e 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -157,6 +157,7 @@ export class InlineChatWidget { } return true; }, + dndContainer: this._elements.root, ..._options.chatWidgetViewOptions }, { @@ -471,7 +472,7 @@ export class InlineChatWidget { } reset() { - this._chatWidget.attachmentModel.clear(); + this._chatWidget.attachmentModel.clear(true); this._chatWidget.saveState(); reset(this._elements.statusLabel); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/code/src/vs/workbench/contrib/inlineChat/browser/utils.ts index 020cd9e0e58..c7db4044055 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/utils.ts @@ -11,6 +11,7 @@ import { IProgress } from '../../../../platform/progress/common/progress.js'; import { IntervalTimer, AsyncIterableSource } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { getNWords } from '../../chat/common/chatWordCounter.js'; +import { TextModelChangeRecorder } from '../../../../editor/contrib/inlineCompletions/browser/model/changeRecorder.js'; @@ -47,9 +48,11 @@ export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdi ? EditOperation.replace(range, part) // first edit needs to override the "anchor" : EditOperation.insert(range.getEndPosition(), part); obs?.start(); - model.pushEditOperations(null, [edit], (undoEdits) => { - progress?.report(undoEdits); - return null; + TextModelChangeRecorder.editWithMetadata({ source: 'inlineChat.applyEdit' }, () => { + model.pushEditOperations(null, [edit], (undoEdits) => { + progress?.report(undoEdits); + return null; + }); }); obs?.stop(); first = false; diff --git a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 3b6979b8f10..99240d7a38d 100644 --- a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -18,7 +18,9 @@ export const enum InlineChatConfigKeys { HoldToSpeech = 'inlineChat.holdToSpeech', AccessibleDiffView = 'inlineChat.accessibleDiffView', LineEmptyHint = 'inlineChat.lineEmptyHint', - LineNLHint = 'inlineChat.lineNaturalLanguageHint' + LineNLHint = 'inlineChat.lineNaturalLanguageHint', + EnableV2 = 'inlineChat.enableV2', + HideOnRequest = 'inlineChat.hideOnRequest' } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -57,6 +59,18 @@ Registry.as(Extensions.Configuration).registerConfigurat type: 'boolean', tags: ['experimental'], }, + [InlineChatConfigKeys.EnableV2]: { + description: localize('enableV2', "Whether to use the next version of inline chat."), + default: false, + type: 'boolean', + tags: ['preview', 'onExp'], + }, + [InlineChatConfigKeys.HideOnRequest]: { + markdownDescription: localize('hideOnRequest', "Whether to hide the inline chat widget after making a request. When enabled, the widget hides after a request has been made and instead the chat overlay shows. When hidden, the widget can always be shown again with the inline chat keybinding or from the chat overlay widget. *Note* that this setting requires `#inlineChat.enableV2#` to be enabled."), + default: false, + type: 'boolean', + tags: ['preview', 'onExp'], + }, } }); diff --git a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 49aa40744ee..f7fedd73265 100644 --- a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -71,7 +71,9 @@ import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputIn import { constObservable, IObservable } from '../../../../../base/common/observable.js'; import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; -import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../../chat/common/constants.js'; +import { IPromptsService } from '../../../chat/common/promptSyntax/service/types.js'; +import { URI } from '../../../../../base/common/uri.js'; suite('InlineChatController', function () { @@ -84,6 +86,7 @@ suite('InlineChatController', function () { name: 'testEditorAgent', isDefault: true, locations: [ChatAgentLocation.Editor], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -195,6 +198,11 @@ suite('InlineChatController', function () { [ILanguageModelsService, new SyncDescriptor(LanguageModelsService)], [ITextModelService, new SyncDescriptor(TextModelResolverService)], [ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)], + [IPromptsService, new class extends mock() { + override async findInstructionFilesFor(_file: readonly URI[]): Promise { + return []; + } + }], ); instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); diff --git a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 5e2024222e7..e5562966c0a 100644 --- a/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/code/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -62,7 +62,8 @@ import { IChatRequestModel } from '../../../chat/common/chatModel.js'; 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 { ChatAgentLocation, ChatMode } 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)], @@ -139,6 +141,7 @@ suite('InlineChatSession', function () { name: 'testAgent', isDefault: true, locations: [ChatAgentLocation.Editor], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], diff --git a/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index 75930b380dd..586e2251675 100644 --- a/code/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/code/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 { 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'; -import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; export class InlineCompletionLanguageStatusBarContribution extends Disposable { public static readonly hot = createHotClass(InlineCompletionLanguageStatusBarContribution); diff --git a/code/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/code/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index c5074ca006a..3d7653e56e1 100644 --- a/code/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts +++ b/code/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -454,14 +454,16 @@ export class BaseIssueReporterService extends Disposable { descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); } - let fileOnExtension, fileOnMarketplace = false; + let fileOnExtension, fileOnMarketplace, fileOnProduct = false; if (value === IssueSource.Extension) { fileOnExtension = true; } else if (value === IssueSource.Marketplace) { fileOnMarketplace = true; + } else if (value === IssueSource.VSCode) { + fileOnProduct = true; } - this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace }); + this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace, fileOnProduct }); this.render(); const title = (this.getElementById('issue-title')).value; @@ -732,7 +734,7 @@ export class BaseIssueReporterService extends Disposable { } else { // If the items property isn't present, the rate limit has been hit const message = $('div.list-title'); - message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait."); + message.textContent = localize('rateLimited', "No duplicate issues found: GitHub query limit exceeded."); similarIssues.appendChild(message); const resetTime = response.headers.get('X-RateLimit-Reset'); @@ -1153,9 +1155,11 @@ export class BaseIssueReporterService extends Disposable { const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + url += this.addTemplateToUrl(gitHubDetails?.owner, gitHubDetails?.repositoryName); + if (url.length > MAX_URL_LENGTH) { try { - url = await this.writeToClipboard(baseUrl, issueBody); + url = await this.writeToClipboard(baseUrl, issueBody) + this.addTemplateToUrl(gitHubDetails?.owner, gitHubDetails?.repositoryName); } catch (_) { console.error('Writing to clipboard failed'); return false; @@ -1176,6 +1180,26 @@ export class BaseIssueReporterService extends Disposable { return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; } + public addTemplateToUrl(owner?: string, repositoryName?: string): string { + const isVscode = this.issueReporterModel.getData().fileOnProduct; + const isCopilot = owner?.toLowerCase() === 'microsoft' && repositoryName === 'vscode-copilot-release'; + const isPython = owner?.toLowerCase() === 'microsoft' && repositoryName === 'vscode-python'; + + if (isVscode) { + return `&template=bug_report.md`; + } + + if (isCopilot) { + return `&template=bug_report_chat.md`; + } + + if (isPython) { + return `&template=bug_report.md`; + } + + return ''; + } + public getIssueUrl(): string { return this.issueReporterModel.fileOnExtension() ? this.getExtensionGitHubUrl() diff --git a/code/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/code/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index bb27e35e45d..ae07c39997a 100644 --- a/code/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/code/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -228,6 +228,8 @@ export class IssueReporter extends BaseIssueReporterService { const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + url += this.addTemplateToUrl(gitHubDetails?.owner, gitHubDetails?.repositoryName); + if (this.data.githubAccessToken && gitHubDetails) { if (await this.submitToGitHub(issueTitle, issueBody, gitHubDetails)) { return true; @@ -236,7 +238,7 @@ export class IssueReporter extends BaseIssueReporterService { try { if (url.length > MAX_URL_LENGTH || issueBody.length > MAX_GITHUB_API_LENGTH) { - url = await this.writeToClipboard(baseUrl, issueBody); + url = await this.writeToClipboard(baseUrl, issueBody) + this.addTemplateToUrl(gitHubDetails?.owner, gitHubDetails?.repositoryName); } } catch (_) { console.error('Writing to clipboard failed'); diff --git a/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts b/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts index 17bff692d74..329707002d5 100644 --- a/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts +++ b/code/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts @@ -308,16 +308,10 @@ class LanguageStatus { left.classList.add('left'); element.appendChild(left); - const label = document.createElement('span'); - label.classList.add('label'); - const labelValue = typeof status.label === 'string' ? status.label : status.label.value; - dom.append(label, ...renderLabelWithIcons(computeText(labelValue, status.busy))); - left.appendChild(label); + const label = typeof status.label === 'string' ? status.label : status.label.value; + dom.append(left, ...renderLabelWithIcons(computeText(label, status.busy))); - const detail = document.createElement('span'); - detail.classList.add('detail'); - this._renderTextPlus(detail, status.detail, store); - left.appendChild(detail); + this._renderTextPlus(left, status.detail, store); const right = document.createElement('div'); right.classList.add('right'); @@ -379,7 +373,12 @@ class LanguageStatus { } private _renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { + let didRenderSeparator = false; for (const node of parseLinkedText(text).nodes) { + if (!didRenderSeparator) { + dom.append(target, dom.$('span.separator')); + didRenderSeparator = true; + } if (typeof node === 'string') { const parts = renderLabelWithIcons(node); dom.append(target, ...parts); diff --git a/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 25433bee4a4..60fc3e78f2b 100644 --- a/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/code/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -87,18 +87,22 @@ flex-grow: 100; } -.monaco-workbench .hover-language-status > .element > .left > .detail:not(:empty)::before { +.monaco-workbench .hover-language-status > .element > .left > .separator::before { content: '\2013'; - padding: 0 4px; + padding: 0 2px; opacity: 0.6; } -.monaco-workbench .hover-language-status > .element > .left > .label:empty { +.monaco-workbench .hover-language-status > .element > .left:empty { display: none; } .monaco-workbench .hover-language-status > .element .left { margin: auto 0; + display: flex; + align-items: center; + gap: 3px; + white-space: nowrap; } .monaco-workbench .hover-language-status > .element .right { diff --git a/code/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts b/code/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index 9dfa4afa29a..4201a6563f1 100644 --- a/code/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts +++ b/code/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -9,7 +9,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } fr import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import * as platform from '../../../../base/common/platform.js'; import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, ILocalExtension, InstallExtensionResult, DidUninstallExtensionEvent } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { INotificationService, NeverShowAgainScope } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, NeverShowAgainScope, NotificationPriority } from '../../../../platform/notification/common/notification.js'; import Severity from '../../../../base/common/severity.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; @@ -73,6 +73,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC }], { sticky: true, + priority: NotificationPriority.URGENT, neverShowAgain: { id: 'langugage.update.donotask', isSecondary: true, scope: NeverShowAgainScope.APPLICATION } } ); @@ -205,6 +206,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC } }], { + priority: NotificationPriority.OPTIONAL, onCancel: () => { logUserReaction('cancelled'); } diff --git a/code/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/code/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index fdd6529ef23..efbf85fa4cf 100644 --- a/code/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/code/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/code/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/code/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 1e5cef15ed4..b4b3aaed20f 100644 --- a/code/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/code/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -22,7 +22,7 @@ import { McpRegistry } from '../common/mcpRegistry.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { McpService } from '../common/mcpService.js'; import { IMcpService } from '../common/mcpTypes.js'; -import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; import { McpUrlHandler } from './mcpUrlHandler.js'; @@ -53,6 +53,7 @@ registerAction2(StopServer); registerAction2(ShowOutput); registerAction2(InstallFromActivation); registerAction2(RestartServer); +registerAction2(ShowConfiguration); registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore); diff --git a/code/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/code/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 6f536d3a7c1..6166392e158 100644 --- a/code/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/code/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -35,6 +35,7 @@ import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { IMcpServer, IMcpService, LazyCollectionState, McpConnectionState, McpServerToolsState } from '../common/mcpTypes.js'; import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js'; import { McpUrlHandler } from './mcpUrlHandler.js'; +import { McpCommandIds } from '../common/mcpCommandIds.js'; // acroynms do not get localized const category: ILocalizedString = { @@ -43,10 +44,9 @@ const category: ILocalizedString = { }; export class ListMcpServerCommand extends Action2 { - public static readonly id = 'workbench.mcp.listServer'; constructor() { super({ - id: ListMcpServerCommand.id, + id: McpCommandIds.ListServer, title: localize2('mcp.list', 'List Servers'), icon: Codicon.server, category, @@ -56,9 +56,9 @@ export class ListMcpServerCommand extends Action2 { ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors), ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent) ), - id: MenuId.ChatInputAttachmentToolbar, + id: MenuId.ChatInput, group: 'navigation', - order: 0 + order: 101 }, }); } @@ -114,19 +114,16 @@ export class ListMcpServerCommand extends Action2 { } else if (picked.id === '$add') { commandService.executeCommand(AddConfigurationAction.ID); } else { - commandService.executeCommand(McpServerOptionsCommand.id, picked.id); + commandService.executeCommand(McpCommandIds.ServerOptions, picked.id); } } } export class McpServerOptionsCommand extends Action2 { - - static readonly id = 'workbench.mcp.serverOptions'; - constructor() { super({ - id: McpServerOptionsCommand.id, + id: McpCommandIds.ServerOptions, title: localize2('mcp.options', 'Server Options'), category, f1: false, @@ -221,8 +218,6 @@ export class McpServerOptionsCommand extends Action2 { } export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution { - public static readonly ID = 'workbench.contrib.mcp.discovery'; - constructor( @IActionViewItemService actionViewItemService: IActionViewItemService, @IMcpService mcpService: IMcpService, @@ -247,6 +242,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo let thisState = DisplayedState.None; switch (server.toolsState.read(reader)) { case McpServerToolsState.Unknown: + case McpServerToolsState.Outdated: if (server.trusted.read(reader) === false) { thisState = DisplayedState.None; } else { @@ -276,7 +272,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo return { state: maxState, servers: serversPerState[maxState] || [] }; }); - this._store.add(actionViewItemService.register(MenuId.ChatInputAttachmentToolbar, ListMcpServerCommand.id, (action, options) => { + this._store.add(actionViewItemService.register(MenuId.ChatInput, McpCommandIds.ListServer, (action, options) => { if (!(action instanceof MenuItemAction)) { return undefined; } @@ -324,17 +320,17 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo const { state, servers } = displayedState.get(); if (state === DisplayedState.NewTools) { - servers.forEach(server => server.start()); + servers.forEach(server => server.stop().then(() => server.start())); mcpService.activateCollections(); } else if (state === DisplayedState.Refreshing) { servers.at(-1)?.showOutput(); } else if (state === DisplayedState.Error) { const server = servers.at(-1); if (server) { - commandService.executeCommand(McpServerOptionsCommand.id, server.definition.id); + commandService.executeCommand(McpCommandIds.ServerOptions, server.definition.id); } } else { - commandService.executeCommand(ListMcpServerCommand.id); + commandService.executeCommand(McpCommandIds.ListServer); } } @@ -362,11 +358,9 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo } export class ResetMcpTrustCommand extends Action2 { - static readonly ID = 'workbench.mcp.resetTrust'; - constructor() { super({ - id: ResetMcpTrustCommand.ID, + id: McpCommandIds.ResetTrust, title: localize2('mcp.resetTrust', "Reset Trust"), category, f1: true, @@ -382,11 +376,9 @@ export class ResetMcpTrustCommand extends Action2 { export class ResetMcpCachedTools extends Action2 { - static readonly ID = 'workbench.mcp.resetCachedTools'; - constructor() { super({ - id: ResetMcpCachedTools.ID, + id: McpCommandIds.ResetCachedTools, title: localize2('mcp.resetCachedTools', "Reset Cached Tools"), category, f1: true, @@ -429,11 +421,9 @@ export class AddConfigurationAction extends Action2 { export class RemoveStoredInput extends Action2 { - static readonly ID = 'workbench.mcp.removeStoredInput'; - constructor() { super({ - id: RemoveStoredInput.ID, + id: McpCommandIds.RemoveStoredInput, title: localize2('mcp.resetCachedTools', "Reset Cached Tools"), category, f1: false, @@ -446,11 +436,9 @@ export class RemoveStoredInput extends Action2 { } export class EditStoredInput extends Action2 { - static readonly ID = 'workbench.mcp.editStoredInput'; - constructor() { super({ - id: EditStoredInput.ID, + id: McpCommandIds.EditStoredInput, title: localize2('mcp.editStoredInput', "Edit Stored Input"), category, f1: false, @@ -463,12 +451,41 @@ export class EditStoredInput extends Action2 { } } -export class ShowOutput extends Action2 { - static readonly ID = 'workbench.mcp.showOutput'; +export class ShowConfiguration extends Action2 { + constructor() { + super({ + id: McpCommandIds.ShowConfiguration, + title: localize2('mcp.command.showConfiguration', "Show Configuration"), + category, + f1: false, + }); + } + run(accessor: ServicesAccessor, collectionId: string, serverId: string): void { + const collection = accessor.get(IMcpRegistry).collections.get().find(c => c.id === collectionId); + if (!collection) { + return; + } + + const server = collection?.serverDefinitions.get().find(s => s.id === serverId); + const editorService = accessor.get(IEditorService); + if (server?.presentation?.origin) { + editorService.openEditor({ + resource: server.presentation.origin.uri, + options: { selection: server.presentation.origin.range } + }); + } else if (collection.presentation?.origin) { + editorService.openEditor({ + resource: collection.presentation.origin, + }); + } + } +} + +export class ShowOutput extends Action2 { constructor() { super({ - id: ShowOutput.ID, + id: McpCommandIds.ShowOutput, title: localize2('mcp.command.showOutput', "Show Output"), category, f1: false, @@ -481,11 +498,9 @@ export class ShowOutput extends Action2 { } export class RestartServer extends Action2 { - static readonly ID = 'workbench.mcp.restartServer'; - constructor() { super({ - id: RestartServer.ID, + id: McpCommandIds.RestartServer, title: localize2('mcp.command.restartServer', "Restart Server"), category, f1: false, @@ -496,16 +511,14 @@ export class RestartServer extends Action2 { const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); s?.showOutput(); await s?.stop(); - await s?.start(); + await s?.start(true); } } export class StartServer extends Action2 { - static readonly ID = 'workbench.mcp.startServer'; - constructor() { super({ - id: StartServer.ID, + id: McpCommandIds.StartServer, title: localize2('mcp.command.startServer', "Start Server"), category, f1: false, @@ -514,16 +527,14 @@ export class StartServer extends Action2 { async run(accessor: ServicesAccessor, serverId: string) { const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); - await s?.start(); + await s?.start(true); } } export class StopServer extends Action2 { - static readonly ID = 'workbench.mcp.stopServer'; - constructor() { super({ - id: StopServer.ID, + id: McpCommandIds.StopServer, title: localize2('mcp.command.stopServer', "Stop Server"), category, f1: false, @@ -537,11 +548,9 @@ export class StopServer extends Action2 { } export class InstallFromActivation extends Action2 { - static readonly ID = 'workbench.mcp.installFromActivation'; - constructor() { super({ - id: InstallFromActivation.ID, + id: McpCommandIds.InstallFromActivation, title: localize2('mcp.command.installFromActivation', "Install..."), category, f1: false, diff --git a/code/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/code/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index f61417a662d..328158592db 100644 --- a/code/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/code/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -16,7 +16,7 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { IMcpConfiguration, IMcpConfigurationSSE, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IMcpConfiguration, IMcpConfigurationHTTP, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -26,13 +26,14 @@ import { IJSONEditingService } from '../../../services/configuration/common/json import { ConfiguredInput } from '../../../services/configurationResolver/common/configurationResolver.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { McpCommandIds } from '../common/mcpCommandIds.js'; import { IMcpConfigurationStdio, mcpConfigurationSection, mcpStdioServerSchema } from '../common/mcpConfiguration.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; -import { McpServerOptionsCommand } from './mcpCommands.js'; +import { IMcpService, McpConnectionState } from '../common/mcpTypes.js'; const enum AddConfigurationType { Stdio, - SSE, + HTTP, NpmPackage, PipPackage, @@ -110,12 +111,13 @@ 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 { const items: QuickPickInput<{ kind: AddConfigurationType } & IQuickPickItem>[] = [ { kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") }, - { kind: AddConfigurationType.SSE, label: localize('mcp.serverType.http', "HTTP (server-sent events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } + { kind: AddConfigurationType.HTTP, label: localize('mcp.serverType.http', "HTTP (HTTP or Server-Sent Events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } ]; let aiSupported: boolean | undefined; @@ -169,7 +171,7 @@ export class McpAddConfigurationCommand { }; } - private async getSSEConfig(): Promise { + private async getSSEConfig(): Promise { const url = await this._quickInputService.input({ title: localize('mcp.url.title', "Enter Server URL"), placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"), @@ -184,10 +186,7 @@ export class McpAddConfigurationCommand { packageType: 'sse' }); - return { - type: 'sse', - url - }; + return { url }; } private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise { @@ -226,7 +225,7 @@ export class McpAddConfigurationCommand { return targetPick?.target; } - private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; config: McpConfigurationServer } | undefined> { + private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record } | undefined> { const packageName = await this._quickInputService.input({ ignoreFocusLost: true, title: assistedTypes[type].title, @@ -302,20 +301,13 @@ export class McpAddConfigurationCommand { return undefined; } - const configWithName = await this._commandService.executeCommand( + return await this._commandService.executeCommand<{ name: string; server: McpConfigurationServer; inputs?: ConfiguredInput[]; inputValues?: Record }>( AddConfigurationCopilotCommand.StartFlow, { name: packageName, type: packageType } ); - - if (!configWithName) { - return undefined; - } - - const { name, ...config } = configWithName; - return { name, config }; } /** Shows the location of a server config once it's discovered. */ @@ -323,9 +315,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, @@ -335,9 +329,15 @@ export class McpAddConfigurationCommand { } }); } else { - this._commandService.executeCommand(McpServerOptionsCommand.id, name); + this._commandService.executeCommand(McpCommandIds.ServerOptions, name); } + server.start(true).then(state => { + if (state.state === McpConnectionState.Kind.Error) { + server.showOutput(); + } + }); + store.dispose(); } })); @@ -364,19 +364,23 @@ export class McpAddConfigurationCommand { // Step 2: Get server details based on type let serverConfig: McpConfigurationServer | undefined; let suggestedName: string | undefined; + let inputs: ConfiguredInput[] | undefined; + let inputValues: Record | undefined; switch (serverType) { case AddConfigurationType.Stdio: serverConfig = await this.getStdioConfig(); break; - case AddConfigurationType.SSE: + case AddConfigurationType.HTTP: serverConfig = await this.getSSEConfig(); break; case AddConfigurationType.NpmPackage: case AddConfigurationType.PipPackage: case AddConfigurationType.DockerImage: { const r = await this.getAssistedConfig(serverType); - serverConfig = r?.config; + serverConfig = r?.server; suggestedName = r?.name; + inputs = r?.inputs; + inputValues = r?.inputValues; break; } default: @@ -411,12 +415,24 @@ export class McpAddConfigurationCommand { : undefined; if (writeToUriDirect) { - await this._jsonEditingService.write(writeToUriDirect, [{ - path: ['servers', serverId], - value: serverConfig - }], true); + await this._jsonEditingService.write(writeToUriDirect, [ + { + path: ['servers', serverId], + value: serverConfig + }, + ...(inputs || []).map(i => ({ + path: ['inputs', -1], + value: i, + })), + ], true); } else { - await this.writeToUserSetting(serverId, serverConfig, target!); + await this.writeToUserSetting(serverId, serverConfig, target!, inputs); + } + + if (inputValues) { + for (const [key, value] of Object.entries(inputValues)) { + await this._mcpRegistry.setSavedInput(key, target ?? ConfigurationTarget.WORKSPACE, value); + } } const packageType = this.getPackageType(serverType); @@ -489,7 +505,7 @@ export class McpAddConfigurationCommand { return 'docker'; case AddConfigurationType.Stdio: return 'stdio'; - case AddConfigurationType.SSE: + case AddConfigurationType.HTTP: return 'sse'; default: return undefined; diff --git a/code/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/code/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts index 8f81edd834f..c9f58f04511 100644 --- a/code/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts +++ b/code/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -19,11 +19,11 @@ import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../platfor import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { McpCommandIds } from '../common/mcpCommandIds.js'; import { IMcpConfigPath, IMcpConfigPathsService } from '../common/mcpConfigPathsService.js'; import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { IMcpService, McpConnectionState } from '../common/mcpTypes.js'; -import { EditStoredInput, RemoveStoredInput, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; const diagnosticOwner = 'vscode.mcp'; @@ -182,14 +182,14 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib lenses.lenses.push({ range, command: { - id: ShowOutput.ID, + id: McpCommandIds.ShowOutput, title: '$(error) ' + localize('server.error', 'Error'), arguments: [server.definition.id], }, }, { range, command: { - id: RestartServer.ID, + id: McpCommandIds.RestartServer, title: localize('mcp.restart', "Restart"), arguments: [server.definition.id], }, @@ -199,14 +199,14 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib lenses.lenses.push({ range, command: { - id: ShowOutput.ID, + id: McpCommandIds.ShowOutput, title: '$(loading~spin) ' + localize('server.starting', 'Starting'), arguments: [server.definition.id], }, }, { range, command: { - id: StopServer.ID, + id: McpCommandIds.StopServer, title: localize('cancel', "Cancel"), arguments: [server.definition.id], }, @@ -216,21 +216,21 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib lenses.lenses.push({ range, command: { - id: ShowOutput.ID, + id: McpCommandIds.ShowOutput, title: '$(check) ' + localize('server.running', 'Running'), arguments: [server.definition.id], }, }, { range, command: { - id: StopServer.ID, + id: McpCommandIds.StopServer, title: localize('mcp.stop', "Stop"), arguments: [server.definition.id], }, }, { range, command: { - id: RestartServer.ID, + id: McpCommandIds.RestartServer, title: localize('mcp.restart', "Restart"), arguments: [server.definition.id], }, @@ -246,7 +246,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib lenses.lenses.push({ range, command: { - id: StartServer.ID, + id: McpCommandIds.StartServer, title: '$(debug-start) ' + localize('mcp.start', "Start"), arguments: [server.definition.id], }, @@ -338,9 +338,9 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib function pushAnnotation(savedId: string, offset: number, saved: IResolvedValue): InlayHint { const tooltip = new MarkdownString([ - markdownCommandLink({ id: EditStoredInput.ID, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), - markdownCommandLink({ id: RemoveStoredInput.ID, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), - markdownCommandLink({ id: RemoveStoredInput.ID, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), + markdownCommandLink({ id: McpCommandIds.EditStoredInput, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), + markdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), + markdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), ].join(' | '), { isTrusted: true }); const hint: InlayHint = { diff --git a/code/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/code/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts index 70969d39a39..46943d3a639 100644 --- a/code/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts +++ b/code/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -118,8 +118,8 @@ export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({ id: `${collectionId}.${name}`, label: name, - launch: 'type' in value && value.type === 'sse' ? { - type: McpServerTransportType.SSE, + launch: 'url' in value ? { + type: McpServerTransportType.HTTP, uri: URI.parse(value.url), headers: Object.entries(value.headers || {}), } : { diff --git a/code/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/code/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts index 0abe6cd9824..e425b5d43a7 100644 --- a/code/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts +++ b/code/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -42,7 +42,7 @@ export function claudeConfigToServerDefinition(idPrefix: string, contents: VSBuf id: `${idPrefix}.${name}`, label: name, launch: server.url ? { - type: McpServerTransportType.SSE, + type: McpServerTransportType.HTTP, uri: URI.parse(server.url), headers: [], } : { diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/code/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts new file mode 100644 index 00000000000..f76e2408b12 --- /dev/null +++ b/code/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Contains all MCP command IDs used in the workbench. + */ +export const enum McpCommandIds { + ListServer = 'workbench.mcp.listServer', + ServerOptions = 'workbench.mcp.serverOptions', + ResetTrust = 'workbench.mcp.resetTrust', + ResetCachedTools = 'workbench.mcp.resetCachedTools', + AddConfiguration = 'workbench.mcp.addConfiguration', + RemoveStoredInput = 'workbench.mcp.removeStoredInput', + EditStoredInput = 'workbench.mcp.editStoredInput', + ShowConfiguration = 'workbench.mcp.showConfiguration', + ShowOutput = 'workbench.mcp.showOutput', + RestartServer = 'workbench.mcp.restartServer', + StartServer = 'workbench.mcp.startServer', + StopServer = 'workbench.mcp.stopServer', + InstallFromActivation = 'workbench.mcp.installFromActivation' +} diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/code/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index cea678906e3..2d3e09b6a4d 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -16,12 +16,6 @@ const mcpActivationEventPrefix = 'onMcpCollection:'; export const mcpActivationEvent = (collectionId: string) => mcpActivationEventPrefix + collectionId; -const mcpSchemaExampleServer = { - command: 'node', - args: ['my-mcp-server.js'], - env: {}, -}; - export const enum DiscoverySource { ClaudeDesktop = 'claude-desktop', Windsurf = 'windsurf', @@ -55,10 +49,17 @@ export const mcpSchemaExampleServers = { } }; +const httpSchemaExamples = { + 'my-mcp-server': { + url: 'http://localhost:3001/mcp', + headers: {}, + } +}; + export const mcpStdioServerSchema: IJSONSchema = { type: 'object', additionalProperties: false, - examples: [mcpSchemaExampleServer], + examples: [mcpSchemaExampleServers['mcp-server-time']], properties: { type: { type: 'string', @@ -103,29 +104,29 @@ export const mcpServerSchema: IJSONSchema = { additionalProperties: false, properties: { servers: { - examples: [mcpSchemaExampleServers], + examples: [ + mcpSchemaExampleServers, + httpSchemaExamples, + ], additionalProperties: { oneOf: [mcpStdioServerSchema, { type: 'object', additionalProperties: false, - required: ['url', 'type'], - examples: [{ - type: 'sse', - url: 'http://localhost:3001', - headers: {}, - }], + required: ['url'], + examples: [httpSchemaExamples['my-mcp-server']], properties: { type: { type: 'string', - enum: ['sse'], + enum: ['http', 'sse'], description: localize('app.mcp.json.type', "The type of the server.") }, url: { type: 'string', format: 'uri', - description: localize('app.mcp.json.url', "The URL of the server-sent-event (SSE) server.") + description: localize('app.mcp.json.url', "The URL of the Streamable HTTP or SSE endpoint.") }, - env: { + headers: { + type: 'object', description: localize('app.mcp.json.headers', "Additional headers sent to the server."), additionalProperties: { type: 'string' }, }, diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts b/code/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts index 0d0aff9aa6d..3cbb821058c 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts @@ -56,7 +56,7 @@ export class McpContextKeysController extends Disposable implements IWorkbenchCo } const toolState = s.toolsState.read(r); - return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.RefreshingFromUnknown; + return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.Outdated || toolState === McpServerToolsState.RefreshingFromUnknown; })); })); } diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/code/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index f7577bcc20c..37266833e8e 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/code/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); @@ -80,10 +92,13 @@ export class McpRegistry extends Disposable implements IMcpRegistry { private readonly _profileStorage = new Lazy(() => this._register(this._instantiationService.createInstance(McpRegistryInputStorage, StorageScope.PROFILE, StorageTarget.USER))); private readonly _trustMemento = new Lazy(() => this._register(createTrustMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, this._storageService))); - private readonly _lazyCollectionsToUpdate = new Set(); 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,8 +121,10 @@ 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 { @@ -130,7 +147,6 @@ export class McpRegistry extends Disposable implements IMcpRegistry { // Incoming collections replace the "lazy" versions. See `ExtensionMcpDiscovery` for an example. if (toReplace) { - this._lazyCollectionsToUpdate.add(collection.id); this._collections.set(currentCollections.map(c => c === toReplace ? collection : c), undefined); } else { this._collections.set([...currentCollections, collection], undefined); @@ -206,6 +222,16 @@ export class McpRegistry extends Disposable implements IMcpRegistry { await this._updateStorageWithExpressionInputs(storage, expr); } + public async setSavedInput(inputId: string, target: ConfigurationTarget, value: string): Promise { + const storage = this._getInputStorageInConfigTarget(target); + const expr = ConfigurationResolverExpression.parse(inputId); + for (const unresolved of expr.unresolved()) { + expr.resolve(unresolved, value); + break; + } + await this._updateStorageWithExpressionInputs(storage, expr); + } + public getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }> { return this._getInputStorage(scope).getMap(); } @@ -246,9 +272,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: [ @@ -304,7 +334,12 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } public async resolveConnection({ collectionRef, definitionRef, forceTrust, logger }: IMcpResolveConnectionOptions): Promise { - const collection = this._collections.get().find(c => c.id === collectionRef.id); + let collection = this._collections.get().find(c => c.id === collectionRef.id); + if (collection?.lazy) { + await collection.lazy.load(); + collection = this._collections.get().find(c => c.id === collectionRef.id); + } + const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id); if (!collection || !definition) { throw new Error(`Collection or definition not found for ${collectionRef.id} and ${definitionRef.id}`); @@ -334,9 +369,16 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } } - let launch: McpServerLaunch | undefined; + let launch: McpServerLaunch | undefined = definition.launch; + if (collection.resolveServerLanch) { + launch = await collection.resolveServerLanch(definition); + if (!launch) { + return undefined; // interaction cancelled by user + } + } + try { - launch = await this._replaceVariablesInLaunch(definition, definition.launch); + launch = await this._replaceVariablesInLaunch(definition, launch); } catch (e) { this._notificationService.notify({ severity: Severity.Error, diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts b/code/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts index 852c34a2a74..91f473aafda 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts +++ b/code/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/code/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/code/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts index 07de8bc1a5b..83d37b7b710 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -72,6 +72,8 @@ export interface IMcpRegistry { clearSavedInputs(scope: StorageScope, inputId?: string): Promise; /** Edits a previously-saved input. */ editSavedInput(inputId: string, folderData: IWorkspaceFolderData | undefined, configSection: string, target: ConfigurationTarget): Promise; + /** Updates a saved input. */ + setSavedInput(inputId: string, target: ConfigurationTarget, value: string): Promise; /** Gets saved inputs from storage. */ getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }>; /** Creates a connection for the collection and definition. */ diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/code/src/vs/workbench/contrib/mcp/common/mcpServer.ts index e565f38cab3..dde5e493c51 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -4,35 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import { raceCancellationError, Sequencer } from '../../../../base/common/async.js'; -import * as json from '../../../../base/common/json.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import * as json from '../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IOutputService } from '../../../services/output/common/output.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { mcpActivationEvent } from './mcpConfiguration.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; -import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; +import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState, McpServerTransportType } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; type ServerBootData = { supportsLogging: boolean; supportsPrompts: boolean; supportsResources: boolean; toolCount: number; + serverName: string; + serverVersion: string; }; type ServerBootClassification = { owner: 'connor4312'; @@ -41,6 +46,8 @@ type ServerBootClassification = { supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the server supports prompts' }; supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the server supports resource' }; toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of tools the server advertises' }; + serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' }; + serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' }; }; type ServerBootState = { @@ -55,6 +62,7 @@ type ServerBootStateClassification = { }; interface IToolCacheEntry { + readonly nonce: string | undefined; /** Cached tools so we can show what's available before it's started */ readonly tools: readonly IValidatedMcpTool[]; } @@ -109,13 +117,13 @@ export class McpServerMetadataCache extends Disposable { } /** Gets cached tools for a server (used before a server is running) */ - getTools(definitionId: string): readonly IValidatedMcpTool[] | undefined { - return this.cache.get(definitionId)?.tools; + getTools(definitionId: string) { + return this.cache.get(definitionId); } /** Sets cached tools for a server */ - storeTools(definitionId: string, tools: readonly IValidatedMcpTool[]): void { - this.cache.set(definitionId, { ...this.cache.get(definitionId), tools }); + storeTools(definitionId: string, nonce: string | undefined, tools: readonly IValidatedMcpTool[]): void { + this.cache.set(definitionId, { ...this.cache.get(definitionId), nonce, tools }); this.didChange = true; } @@ -154,17 +162,33 @@ 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; public readonly toolsState = derived(reader => { + const currentNonce = () => this._mcpRegistry.collections.read(reader) + .find(c => c.id === this.collection.id) + ?.serverDefinitions.read(reader) + .find(d => d.id === this.definition.id) + ?.cacheNonce; + const stateWhenServingFromCache = () => { + if (!this.toolsFromCache) { + return McpServerToolsState.Unknown; + } + + return currentNonce() === this.toolsFromCache.nonce ? McpServerToolsState.Cached : McpServerToolsState.Outdated; + }; + const fromServer = this.toolsFromServerPromise.read(reader); const connectionState = this.connectionState.read(reader); const isIdle = McpConnectionState.canBeStarted(connectionState.state) && !fromServer; if (isIdle) { - return this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown; + return stateWhenServingFromCache(); } const fromServerResult = fromServer?.promiseResult.read(reader); @@ -172,7 +196,11 @@ export class McpServer extends Disposable implements IMcpServer { return this.toolsFromCache ? McpServerToolsState.RefreshingFromCached : McpServerToolsState.RefreshingFromUnknown; } - return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live; + if (fromServerResult.error) { + return stateWhenServingFromCache(); + } + + return fromServerResult.data?.nonce === currentNonce() ? McpServerToolsState.Live : McpServerToolsState.Outdated; }); private readonly _loggerId: string; @@ -196,6 +224,8 @@ export class McpServer extends Disposable implements IMcpServer { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ICommandService private readonly _commandService: ICommandService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @INotificationService private readonly _notificationService: INotificationService, + @IOpenerService private readonly _openerService: IOpenerService, ) { super(); @@ -228,29 +258,22 @@ export class McpServer extends Disposable implements IMcpServer { // 2. Populate this.tools when we connect to a server. this._register(autorunWithStore((reader, store) => { - const cnx = this._connection.read(reader)?.handler.read(reader); - if (cnx) { - this.populateLiveData(cnx, store); + const cnx = this._connection.read(reader); + const handler = cnx?.handler.read(reader); + if (handler) { + this.populateLiveData(handler, cnx?.definition.cacheNonce, store); } else { this.resetLiveData(); } })); - // 3. Update the cache when tools update - this._register(autorun(reader => { - const tools = this.toolsFromServer.read(reader); - if (tools) { - this._toolCache.storeTools(definition.id, tools); - } - })); - - // 4. Publish tools + // 3. Publish tools const toolPrefix = this._mcpRegistry.collectionToolPrefix(this.collection); this.tools = derived(reader => { const serverTools = this.toolsFromServer.read(reader); - const definitions = serverTools ?? this.toolsFromCache ?? []; + const definitions = serverTools?.tools ?? this.toolsFromCache?.tools ?? []; const prefix = toolPrefix.read(reader); - return definitions.map(def => new McpTool(this, prefix, def)); + return definitions.map(def => new McpTool(this, prefix, def)).sort((a, b) => a.compare(b)); }); } @@ -306,10 +329,44 @@ export class McpServer extends Disposable implements IMcpServer { time: Date.now() - start, }); + if (state.state === McpConnectionState.Kind.Error && isFromInteraction) { + this.showInteractiveError(connection, state); + } + return state; }); } + private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error) { + if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) { + let docsLink: string | undefined; + switch (cnx.launchDefinition.command) { + case 'uvx': + docsLink = `https://aka.ms/vscode-mcp-install/uvx`; + break; + case 'npx': + docsLink = `https://aka.ms/vscode-mcp-install/npx`; + break; + } + + const options: IPromptChoice[] = [{ + label: localize('mcp.command.showOutput', "Show Output"), + run: () => this.showOutput(), + }]; + + if (docsLink) { + options.push({ + label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command), + run: () => this._openerService.open(URI.parse(docsLink)), + }); + } + + this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options); + } else { + this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message)); + } + } + public stop(): Promise { return this._connection.get()?.stop() || Promise.resolve(); } @@ -384,7 +441,7 @@ export class McpServer extends Disposable implements IMcpServer { return validated; } - private populateLiveData(handler: McpServerRequestHandler, store: DisposableStore) { + private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) { const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); @@ -394,11 +451,11 @@ export class McpServer extends Disposable implements IMcpServer { const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]); const toolPromiseSafe = toolPromise.then(async tools => { handler.logger.info(`Discovered ${tools.length} tools`); - return this._getValidatedTools(handler, tools); + return { tools: await this._getValidatedTools(handler, tools), nonce: cacheNonce }; }); this.toolsFromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx); - return [toolPromise]; + return [toolPromiseSafe]; }; store.add(handler.onDidChangeToolList(() => { @@ -411,12 +468,16 @@ export class McpServer extends Disposable implements IMcpServer { promises = updateTools(tx); }); - Promise.all(promises!).then(([tools]) => { + Promise.all(promises!).then(([{ tools }]) => { + this._toolCache.storeTools(this.definition.id, cacheNonce, tools); + this._telemetryService.publicLog2('mcp/serverBoot', { supportsLogging: !!handler.capabilities.logging, supportsPrompts: !!handler.capabilities.prompts, supportsResources: !!handler.capabilities.resources, toolCount: tools.length, + serverName: handler.serverInfo.name, + serverVersion: handler.serverInfo.version, }); }); } @@ -426,7 +487,6 @@ export class McpServer extends Disposable implements IMcpServer { * connection started if it is not already. */ public async callOn(fn: (handler: McpServerRequestHandler) => Promise, token: CancellationToken = CancellationToken.None): Promise { - await this.start(); // idempotent let ranOnce = false; @@ -481,7 +541,46 @@ export class McpTool implements IMcpTool { call(params: Record, token?: CancellationToken): Promise { // 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); + return this._server.callOn(h => h.callTool({ name, arguments: params }, token), token); + } + + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise { + return this._callWithProgress(params, progress, token); + } + + _callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken, allowRetry = true): Promise { + // 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; + const progressToken = generateUuid(); + + return this._server.callOn(h => { + let lastProgressN = 0; + const listener = h.onDidReceiveProgressNotification((e) => { + if (e.params.progressToken === progressToken) { + progress.report({ + message: e.params.message, + increment: e.params.progress - lastProgressN, + total: e.params.total, + }); + lastProgressN = e.params.progress; + } + }); + + return h.callTool({ name, arguments: params, _meta: { progressToken } }, token) + .finally(() => listener.dispose()) + .catch(err => { + const state = this._server.connectionState.get(); + if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) { + return this._callWithProgress(params, progress, token, false); + } else { + throw err; + } + }); + }, token); + } + + compare(other: IMcpTool): number { + return this._definition.name.localeCompare(other.definition.name); } } diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/code/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts index f367162fd6b..30fff6f2f16 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts +++ b/code/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/code/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/code/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts index 8c0fa08cae0..30ae386b251 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -57,6 +57,10 @@ export class McpServerRequestHandler extends Disposable { return this._serverInit.capabilities; } + public get serverInfo(): MCP.Implementation { + return this._serverInit.serverInfo; + } + // Event emitters for server notifications private readonly _onDidReceiveCancelledNotification = this._register(new Emitter()); readonly onDidReceiveCancelledNotification = this._onDidReceiveCancelledNotification.event; @@ -466,7 +470,7 @@ export class McpServerRequestHandler extends Disposable { /** * Call a specific tool */ - callTool(params: MCP.CallToolRequest['params'], token?: CancellationToken): Promise { + callTool(params: MCP.CallToolRequest['params'] & MCP.Request['params'], token?: CancellationToken): Promise { return this.sendRequest({ method: 'tools/call', params }, token); } diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpService.ts b/code/src/vs/workbench/contrib/mcp/common/mcpService.ts index 38666d83ca1..4ea7121fc91 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { decodeBase64 } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, DisposableStore, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; import { equals } from '../../../../base/common/objects.js'; import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; @@ -15,15 +16,15 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { McpCommandIds } from './mcpCommandIds.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServer, McpServerMetadataCache } from './mcpServer.js'; import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; interface ISyncedToolData { toolData: IToolData; - toolDispose: IDisposable; - implDispose: IDisposable; + store: DisposableStore; } type IMcpServerRec = IReference; @@ -98,40 +99,46 @@ export class McpService extends Disposable implements IMcpService { const collection = this._mcpRegistry.collections.get().find(c => c.id === server.collection.id); const toolData: IToolData = { id: tool.id, - source: { type: 'mcp', collectionId: server.collection.id, definitionId: server.definition.id }, + source: { type: 'mcp', label: server.definition.label, collectionId: server.collection.id, definitionId: server.definition.id }, icon: Codicon.tools, - displayName: tool.definition.name, + displayName: tool.definition.annotations?.title || tool.definition.name, toolReferenceName: tool.definition.name, modelDescription: tool.definition.description ?? '', userDescription: tool.definition.description ?? '', inputSchema: tool.definition.inputSchema, canBeReferencedInPrompt: true, supportsToolPicker: true, + alwaysDisplayInputOutput: true, runsInWorkspace: collection?.scope === StorageScope.WORKSPACE || !!collection?.remoteAuthority, tags: ['mcp'], }; + const registerTool = (store: DisposableStore) => { + store.add(this._toolsService.registerToolData(toolData)); + store.add(this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server))); + }; + + if (existing) { if (!equals(existing.toolData, toolData)) { existing.toolData = toolData; - existing.toolDispose.dispose(); - existing.toolDispose = this._toolsService.registerToolData(toolData); + existing.store.clear(); + // We need to re-register both the data and implementation, as the + // implementation is discarded when the data is removed (#245921) + registerTool(store); } toDelete.delete(tool.id); } else { - tools.set(tool.id, { - toolData, - toolDispose: this._toolsService.registerToolData(toolData), - implDispose: this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server)), - }); + const store = new DisposableStore(); + registerTool(store); + tools.set(tool.id, { toolData, store }); } } for (const id of toDelete) { const tool = tools.get(id); if (tool) { - tool.toolDispose.dispose(); - tool.implDispose.dispose(); + tool.store.dispose(); tools.delete(id); } } @@ -139,8 +146,7 @@ export class McpService extends Disposable implements IMcpService { store.add(toDisposable(() => { for (const tool of tools.values()) { - tool.toolDispose.dispose(); - tool.implDispose.dispose(); + tool.store.dispose(); } })); } @@ -222,20 +228,28 @@ class McpToolImplementation implements IToolImpl { const mcpToolWarning = localize( 'mcp.tool.warning', - "{0} This tool is from \'{1}\' (MCP Server). Note that MCP servers or malicious conversation content may attempt to misuse '{2}' through tools. Please carefully review any requested actions.", + "{0} Note that MCP servers or malicious conversation content may attempt to misuse '{1}' through tools.", '$(info)', - server.definition.label, this._productService.nameShort ); + const needsConfirmation = !tool.definition.annotations?.readOnlyHint; + const title = tool.definition.annotations?.title || ('`' + tool.definition.name + '`'); + const subtitle = localize('msg.subtitle', "{0} (MCP Server)", server.definition.label); + return { - confirmationMessages: { - title: localize('msg.title', "Run `{0}`", tool.definition.name, server.definition.label), + confirmationMessages: needsConfirmation ? { + title: localize('msg.title', "Run {0}", title), message: new MarkdownString(localize('msg.msg', "{0}\n\n {1}", tool.definition.description, mcpToolWarning), { supportThemeIcons: true }), allowAutoConfirm: true, - }, - invocationMessage: new MarkdownString(localize('msg.run', "Running `{0}`", tool.definition.name, server.definition.label)), - pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran `{0}` ", tool.definition.name, server.definition.label)), + } : undefined, + invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)), + pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran {0} ", title)), + originMessage: new MarkdownString(markdownCommandLink({ + id: McpCommandIds.ShowConfiguration, + title: subtitle, + arguments: [server.collection.id, server.definition.id], + }), { isTrusted: true }), toolSpecificData: { kind: 'input', rawInput: parameters @@ -243,33 +257,38 @@ class McpToolImplementation implements IToolImpl { }; } - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken) { const result: IToolResult = { content: [] }; - const outputParts: string[] = []; + const callResult = await this._tool.callWithProgress(invocation.parameters as Record, progress, token); + const details: IToolResultInputOutputDetails = { + input: JSON.stringify(invocation.parameters, undefined, 2), + output: [], + isError: callResult.isError === true, + }; - const callResult = await this._tool.call(invocation.parameters as Record, token); for (const item of callResult.content) { if (item.type === 'text') { + details.output.push({ type: 'text', value: item.text }); result.content.push({ kind: 'text', value: item.text }); - - outputParts.push(item.text); + } else if (item.type === 'image' || item.type === 'audio') { + details.output.push({ type: 'data', mimeType: item.mimeType, value64: item.data }); + result.content.push({ + kind: 'data', + value: { mimeType: item.mimeType, data: decodeBase64(item.data) } + }); } else { - // TODO@jrieken handle different item types + // unsupported for now. } } - result.toolResultDetails = { - input: JSON.stringify(invocation.parameters, undefined, 2), - output: outputParts.join('\n') - }; - + result.toolResultDetails = details; return result; } } diff --git a/code/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/code/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 6c4a2540878..f43dc1472ff 100644 --- a/code/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/code/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { assertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { equals as objectsEqual } from '../../../../base/common/objects.js'; -import { equals as arraysEqual } from '../../../../base/common/arrays.js'; import { IObservable } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Location } from '../../../../editor/common/languages.js'; @@ -17,6 +17,7 @@ import { ExtensionIdentifier } from '../../../../platform/extensions/common/exte import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; @@ -44,6 +45,9 @@ export interface McpCollectionDefinition { /** Scope where associated collection info should be stored. */ readonly scope: StorageScope; + /** Resolves a server definition. If present, always called before a server starts. */ + resolveServerLanch?(definition: McpServerDefinition): Promise; + /** For lazy-loaded collections only: */ readonly lazy?: { /** True if `serverDefinitions` were loaded from the cache */ @@ -78,6 +82,8 @@ export namespace McpCollectionDefinition { readonly label: string; readonly isTrustedByDefault: boolean; readonly scope: StorageScope; + readonly canResolveLaunch: boolean; + readonly extensionId: string; } export function equals(a: McpCollectionDefinition, b: McpCollectionDefinition): boolean { @@ -99,6 +105,8 @@ export interface McpServerDefinition { readonly roots?: URI[] | undefined; /** If set, allows configuration variables to be resolved in the {@link launch} with the given context */ readonly variableReplacement?: McpServerDefinitionVariableReplacement; + /** Nonce used for caching the server. Changing the nonce will indicate that tools need to be refreshed. */ + readonly cacheNonce?: string; readonly presentation?: { /** Sort order of the definition. */ @@ -112,6 +120,7 @@ export namespace McpServerDefinition { export interface Serialized { readonly id: string; readonly label: string; + readonly cacheNonce?: string; readonly launch: McpServerLaunch.Serialized; readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; } @@ -124,6 +133,7 @@ export namespace McpServerDefinition { return { id: def.id, label: def.label, + cacheNonce: def.cacheNonce, launch: McpServerLaunch.fromSerialized(def.launch), variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; @@ -228,6 +238,8 @@ export const enum McpServerToolsState { Unknown, /** Tools were read from the cache */ Cached, + /** Tools were read from the cache or live, but they may be outdated. */ + Outdated, /** Tools are refreshing for the first time */ RefreshingFromUnknown, /** Tools are refreshing and the current tools are cached */ @@ -248,13 +260,18 @@ export interface IMcpTool { * @throws {@link McpConnectionFailedError} if the connection to the server fails */ call(params: Record, token?: CancellationToken): Promise; + + /** + * Identical to {@link call}, but reports progress. + */ + callWithProgress(params: Record, progress: ToolProgress, token?: CancellationToken): Promise; } export const enum McpServerTransportType { /** A command-line MCP server communicating over standard in/out */ Stdio = 1 << 0, /** An MCP server that uses Server-Sent Events */ - SSE = 1 << 1, + HTTP = 1 << 1, } /** @@ -271,22 +288,23 @@ export interface McpServerTransportStdio { } /** - * MCP server launched on the command line which communicated over server-sent-events. + * MCP server launched on the command line which communicated over SSE or Streamable HTTP. * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse + * https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http */ -export interface McpServerTransportSSE { - readonly type: McpServerTransportType.SSE; +export interface McpServerTransportHTTP { + readonly type: McpServerTransportType.HTTP; readonly uri: URI; readonly headers: [string, string][]; } export type McpServerLaunch = | McpServerTransportStdio - | McpServerTransportSSE; + | McpServerTransportHTTP; export namespace McpServerLaunch { export type Serialized = - | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } + | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][] } | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { @@ -295,7 +313,7 @@ export namespace McpServerLaunch { export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { switch (launch.type) { - case McpServerTransportType.SSE: + case McpServerTransportType.HTTP: return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; case McpServerTransportType.Stdio: return { @@ -320,6 +338,12 @@ export interface IMcpServerConnection extends IDisposable { readonly state: IObservable; readonly handler: IObservable; + /** + * Resolved launch definition. Might not match the `definition.launch` due to + * resolution logic in extension-provided MCPs. + */ + readonly launchDefinition: McpServerLaunch; + /** * Starts the server if it's stopped. Returns a promise that resolves once * server exits a 'starting' state. @@ -394,6 +418,8 @@ export namespace McpConnectionState { export interface Error { readonly state: Kind.Error; + readonly code?: string; + readonly shouldRetry?: boolean; readonly message: string; } } diff --git a/code/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/code/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts index 582e69eaf93..b5f8eeeff62 100644 --- a/code/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts +++ b/code/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable @stylistic/ts/member-delimiter-style */ /* eslint-disable local/code-no-unexternalized-strings */ /** @@ -13,24 +12,38 @@ * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ */ export namespace MCP { - /* JSON-RPC types */ + /** + * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. + */ export type JSONRPCMessage = | JSONRPCRequest | JSONRPCNotification + | JSONRPCBatchRequest | JSONRPCResponse - | JSONRPCError; + | JSONRPCError + | JSONRPCBatchResponse; + + /** + * A JSON-RPC batch request, as described in https://www.jsonrpc.org/specification#batch. + */ + export type JSONRPCBatchRequest = (JSONRPCRequest | JSONRPCNotification)[]; - export const LATEST_PROTOCOL_VERSION = "2024-11-05"; + /** + * A JSON-RPC batch response, as described in https://www.jsonrpc.org/specification#batch. + */ + export type JSONRPCBatchResponse = (JSONRPCResponse | JSONRPCError)[]; + + export const LATEST_PROTOCOL_VERSION = "2025-03-26"; export const JSONRPC_VERSION = "2.0"; /** - * A progress token, used to associate progress notifications with the original request. - */ + * A progress token, used to associate progress notifications with the original request. + */ export type ProgressToken = string | number; /** - * An opaque token used to represent a cursor for pagination. - */ + * An opaque token used to represent a cursor for pagination. + */ export type Cursor = string; export interface Request { @@ -66,28 +79,28 @@ export namespace MCP { } /** - * A uniquely identifying ID for a request in JSON-RPC. - */ + * A uniquely identifying ID for a request in JSON-RPC. + */ export type RequestId = string | number; /** - * A request that expects a response. - */ + * A request that expects a response. + */ export interface JSONRPCRequest extends Request { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; } /** - * A notification which does not expect a response. - */ + * A notification which does not expect a response. + */ export interface JSONRPCNotification extends Notification { jsonrpc: typeof JSONRPC_VERSION; } /** - * A successful (non-error) response to a request. - */ + * A successful (non-error) response to a request. + */ export interface JSONRPCResponse { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; @@ -102,8 +115,8 @@ export namespace MCP { export const INTERNAL_ERROR = -32603; /** - * A response to a request that indicates an error occurred. - */ + * A response to a request that indicates an error occurred. + */ export interface JSONRPCError { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; @@ -125,20 +138,20 @@ export namespace MCP { /* Empty result */ /** - * A response that indicates success but carries no data. - */ + * A response that indicates success but carries no data. + */ export type EmptyResult = Result; /* Cancellation */ /** - * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its `initialize` request. - */ + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + */ export interface CancelledNotification extends Notification { method: "notifications/cancelled"; params: { @@ -158,8 +171,8 @@ export namespace MCP { /* Initialization */ /** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - */ + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ export interface InitializeRequest extends Request { method: "initialize"; params: { @@ -173,8 +186,8 @@ export namespace MCP { } /** - * After receiving an initialize request from the client, the server sends this response. - */ + * After receiving an initialize request from the client, the server sends this response. + */ export interface InitializeResult extends Result { /** * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. @@ -182,6 +195,7 @@ export namespace MCP { protocolVersion: string; capabilities: ServerCapabilities; serverInfo: Implementation; + /** * Instructions describing how to use the server and its features. * @@ -191,15 +205,15 @@ export namespace MCP { } /** - * This notification is sent from the client to the server after initialization has finished. - */ + * This notification is sent from the client to the server after initialization has finished. + */ export interface InitializedNotification extends Notification { method: "notifications/initialized"; } /** - * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - */ + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + */ export interface ClientCapabilities { /** * Experimental, non-standard capabilities that the client supports. @@ -221,8 +235,8 @@ export namespace MCP { } /** - * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - */ + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + */ export interface ServerCapabilities { /** * Experimental, non-standard capabilities that the server supports. @@ -232,6 +246,10 @@ export namespace MCP { * Present if the server supports sending log messages to the client. */ logging?: object; + /** + * Present if the server supports argument autocompletion suggestions. + */ + completions?: object; /** * Present if the server offers any prompt templates. */ @@ -266,8 +284,8 @@ export namespace MCP { } /** - * Describes the name and version of an MCP implementation. - */ + * Describes the name and version of an MCP implementation. + */ export interface Implementation { name: string; version: string; @@ -275,16 +293,16 @@ export namespace MCP { /* Ping */ /** - * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. - */ + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + */ export interface PingRequest extends Request { method: "ping"; } /* Progress notifications */ /** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - */ + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + */ export interface ProgressNotification extends Notification { method: "notifications/progress"; params: { @@ -304,6 +322,10 @@ export namespace MCP { * @TJS-type number */ total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; }; } @@ -328,36 +350,36 @@ export namespace MCP { /* Resources */ /** - * Sent from the client to request a list of resources the server has. - */ + * Sent from the client to request a list of resources the server has. + */ export interface ListResourcesRequest extends PaginatedRequest { method: "resources/list"; } /** - * The server's response to a resources/list request from the client. - */ + * The server's response to a resources/list request from the client. + */ export interface ListResourcesResult extends PaginatedResult { resources: Resource[]; } /** - * Sent from the client to request a list of resource templates the server has. - */ + * Sent from the client to request a list of resource templates the server has. + */ export interface ListResourceTemplatesRequest extends PaginatedRequest { method: "resources/templates/list"; } /** - * The server's response to a resources/templates/list request from the client. - */ + * The server's response to a resources/templates/list request from the client. + */ export interface ListResourceTemplatesResult extends PaginatedResult { resourceTemplates: ResourceTemplate[]; } /** - * Sent from the client to the server, to read a specific resource URI. - */ + * Sent from the client to the server, to read a specific resource URI. + */ export interface ReadResourceRequest extends Request { method: "resources/read"; params: { @@ -371,22 +393,22 @@ export namespace MCP { } /** - * The server's response to a resources/read request from the client. - */ + * The server's response to a resources/read request from the client. + */ export interface ReadResourceResult extends Result { contents: (TextResourceContents | BlobResourceContents)[]; } /** - * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + */ export interface ResourceListChangedNotification extends Notification { method: "notifications/resources/list_changed"; } /** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. - */ + * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + */ export interface SubscribeRequest extends Request { method: "resources/subscribe"; params: { @@ -400,8 +422,8 @@ export namespace MCP { } /** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. - */ + * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + */ export interface UnsubscribeRequest extends Request { method: "resources/unsubscribe"; params: { @@ -415,8 +437,8 @@ export namespace MCP { } /** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. - */ + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + */ export interface ResourceUpdatedNotification extends Notification { method: "notifications/resources/updated"; params: { @@ -430,9 +452,9 @@ export namespace MCP { } /** - * A known resource that the server is capable of reading. - */ - export interface Resource extends Annotated { + * A known resource that the server is capable of reading. + */ + export interface Resource { /** * The URI of this resource. * @@ -459,6 +481,11 @@ export namespace MCP { */ mimeType?: string; + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + /** * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. * @@ -468,9 +495,9 @@ export namespace MCP { } /** - * A template description for resources available on the server. - */ - export interface ResourceTemplate extends Annotated { + * A template description for resources available on the server. + */ + export interface ResourceTemplate { /** * A URI template (according to RFC 6570) that can be used to construct resource URIs. * @@ -496,11 +523,16 @@ export namespace MCP { * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. */ mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * The contents of a specific resource or sub-resource. - */ + * The contents of a specific resource or sub-resource. + */ export interface ResourceContents { /** * The URI of this resource. @@ -532,22 +564,22 @@ export namespace MCP { /* Prompts */ /** - * Sent from the client to request a list of prompts and prompt templates the server has. - */ + * Sent from the client to request a list of prompts and prompt templates the server has. + */ export interface ListPromptsRequest extends PaginatedRequest { method: "prompts/list"; } /** - * The server's response to a prompts/list request from the client. - */ + * The server's response to a prompts/list request from the client. + */ export interface ListPromptsResult extends PaginatedResult { prompts: Prompt[]; } /** - * Used by the client to get a prompt provided by the server. - */ + * Used by the client to get a prompt provided by the server. + */ export interface GetPromptRequest extends Request { method: "prompts/get"; params: { @@ -563,8 +595,8 @@ export namespace MCP { } /** - * The server's response to a prompts/get request from the client. - */ + * The server's response to a prompts/get request from the client. + */ export interface GetPromptResult extends Result { /** * An optional description for the prompt. @@ -574,8 +606,8 @@ export namespace MCP { } /** - * A prompt or prompt template that the server offers. - */ + * A prompt or prompt template that the server offers. + */ export interface Prompt { /** * The name of the prompt or prompt template. @@ -592,8 +624,8 @@ export namespace MCP { } /** - * Describes an argument that a prompt can accept. - */ + * Describes an argument that a prompt can accept. + */ export interface PromptArgument { /** * The name of the argument. @@ -610,68 +642,73 @@ export namespace MCP { } /** - * The sender or recipient of messages and data in a conversation. - */ + * The sender or recipient of messages and data in a conversation. + */ export type Role = "user" | "assistant"; /** - * Describes a message returned as part of a prompt. - * - * This is similar to `SamplingMessage`, but also supports the embedding of - * resources from the MCP server. - */ + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + */ export interface PromptMessage { role: Role; - content: TextContent | ImageContent | EmbeddedResource; + content: TextContent | ImageContent | AudioContent | EmbeddedResource; } /** - * The contents of a resource, embedded into a prompt or tool call result. - * - * It is up to the client how best to render embedded resources for the benefit - * of the LLM and/or the user. - */ - export interface EmbeddedResource extends Annotated { + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + */ + export interface EmbeddedResource { type: "resource"; resource: TextResourceContents | BlobResourceContents; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + */ export interface PromptListChangedNotification extends Notification { method: "notifications/prompts/list_changed"; } /* Tools */ /** - * Sent from the client to request a list of tools the server has. - */ + * Sent from the client to request a list of tools the server has. + */ export interface ListToolsRequest extends PaginatedRequest { method: "tools/list"; } /** - * The server's response to a tools/list request from the client. - */ + * The server's response to a tools/list request from the client. + */ export interface ListToolsResult extends PaginatedResult { tools: Tool[]; } /** - * The server's response to a tool call. - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to true, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. - */ + * The server's response to a tool call. + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ export interface CallToolResult extends Result { - content: (TextContent | ImageContent | EmbeddedResource)[]; + content: (TextContent | ImageContent | AudioContent | EmbeddedResource)[]; /** * Whether the tool call ended in an error. @@ -682,8 +719,8 @@ export namespace MCP { } /** - * Used by the client to invoke a tool provided by the server. - */ + * Used by the client to invoke a tool provided by the server. + */ export interface CallToolRequest extends Request { method: "tools/call"; params: { @@ -693,24 +730,82 @@ export namespace MCP { } /** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + */ export interface ToolListChangedNotification extends Notification { method: "notifications/tools/list_changed"; } /** - * Definition for a tool the client can call. - */ + * Additional properties describing a Tool to clients. + * + * NOTE: all properties in ToolAnnotations are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on ToolAnnotations + * received from untrusted servers. + */ + export interface ToolAnnotations { + /** + * A human-readable title for the tool. + */ + title?: string; + + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint?: boolean; + + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint?: boolean; + + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on the its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint?: boolean; + + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint?: boolean; + } + + /** + * Definition for a tool the client can call. + */ export interface Tool { /** * The name of the tool. */ name: string; + /** * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. */ description?: string; + /** * A JSON Schema object defining the expected parameters for the tool. */ @@ -719,12 +814,17 @@ export namespace MCP { properties?: { [key: string]: object }; required?: string[]; }; + + /** + * Optional additional tool information. + */ + annotations?: ToolAnnotations; } /* Logging */ /** - * A request from the client to the server, to enable or adjust logging. - */ + * A request from the client to the server, to enable or adjust logging. + */ export interface SetLevelRequest extends Request { method: "logging/setLevel"; params: { @@ -736,8 +836,8 @@ export namespace MCP { } /** - * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. - */ + * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + */ export interface LoggingMessageNotification extends Notification { method: "notifications/message"; params: { @@ -757,11 +857,11 @@ export namespace MCP { } /** - * The severity of a log message. - * - * These map to syslog message severities, as specified in RFC-5424: - * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - */ + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + */ export type LoggingLevel = | "debug" | "info" @@ -774,8 +874,8 @@ export namespace MCP { /* Sampling */ /** - * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - */ + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + */ export interface CreateMessageRequest extends Request { method: "sampling/createMessage"; params: { @@ -809,8 +909,8 @@ export namespace MCP { } /** - * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. - */ + * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. + */ export interface CreateMessageResult extends Result, SamplingMessage { /** * The name of the model that generated the message. @@ -823,81 +923,116 @@ export namespace MCP { } /** - * Describes a message issued to or received from an LLM API. - */ + * Describes a message issued to or received from an LLM API. + */ export interface SamplingMessage { role: Role; - content: TextContent | ImageContent; + content: TextContent | ImageContent | AudioContent; } /** - * Base for objects that include optional annotations for the client. The client can use annotations to inform how objects are used or displayed - */ - export interface Annotated { - annotations?: { - /** - * Describes who the intended customer of this object or data is. - * - * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). - */ - audience?: Role[]; + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ + export interface Annotations { + /** + * Describes who the intended customer of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; - /** - * Describes how important this data is for operating the server. - * - * A value of 1 means "most important," and indicates that the data is - * effectively required, while 0 means "least important," and indicates that - * the data is entirely optional. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - priority?: number; - } + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; } /** - * Text provided to or from an LLM. - */ - export interface TextContent extends Annotated { + * Text provided to or from an LLM. + */ + export interface TextContent { type: "text"; + /** * The text content of the message. */ text: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * An image provided to or from an LLM. - */ - export interface ImageContent extends Annotated { + * An image provided to or from an LLM. + */ + export interface ImageContent { type: "image"; + /** * The base64-encoded image data. * * @format byte */ data: string; + /** * The MIME type of the image. Different providers may support different image types. */ mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * The server's preferences for model selection, requested of the client during sampling. - * - * Because LLMs can vary along multiple dimensions, choosing the "best" model is - * rarely straightforward. Different models excel in different areas-some are - * faster but less capable, others are more capable but more expensive, and so - * on. This interface allows servers to express their priorities across multiple - * dimensions to help clients make an appropriate selection for their use case. - * - * These preferences are always advisory. The client MAY ignore them. It is also - * up to the client to decide how to interpret these preferences and how to - * balance them against other considerations. - */ + * Audio provided to or from an LLM. + */ + export interface AudioContent { + type: "audio"; + + /** + * The base64-encoded audio data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + } + + /** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas-some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + */ export interface ModelPreferences { /** * Optional hints to use for model selection. @@ -945,11 +1080,11 @@ export namespace MCP { } /** - * Hints to use for model selection. - * - * Keys not declared here are currently left unspecified by the spec and are up - * to the client to interpret. - */ + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + */ export interface ModelHint { /** * A hint for a model name. @@ -967,8 +1102,8 @@ export namespace MCP { /* Autocomplete */ /** - * A request from the client to the server, to ask for completion options. - */ + * A request from the client to the server, to ask for completion options. + */ export interface CompleteRequest extends Request { method: "completion/complete"; params: { @@ -990,8 +1125,8 @@ export namespace MCP { } /** - * The server's response to a completion/complete request - */ + * The server's response to a completion/complete request + */ export interface CompleteResult extends Result { completion: { /** @@ -1010,8 +1145,8 @@ export namespace MCP { } /** - * A reference to a resource or resource template definition. - */ + * A reference to a resource or resource template definition. + */ export interface ResourceReference { type: "ref/resource"; /** @@ -1023,8 +1158,8 @@ export namespace MCP { } /** - * Identifies a prompt. - */ + * Identifies a prompt. + */ export interface PromptReference { type: "ref/prompt"; /** @@ -1035,30 +1170,30 @@ export namespace MCP { /* Roots */ /** - * Sent from the server to request a list of root URIs from the client. Roots allow - * servers to ask for specific directories or files to operate on. A common example - * for roots is providing a set of repositories or directories a server should operate - * on. - * - * This request is typically used when the server needs to understand the file system - * structure or access specific locations that the client has permission to read from. - */ + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + */ export interface ListRootsRequest extends Request { method: "roots/list"; } /** - * The client's response to a roots/list request from the server. - * This result contains an array of Root objects, each representing a root directory - * or file that the server can operate on. - */ + * The client's response to a roots/list request from the server. + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. + */ export interface ListRootsResult extends Result { roots: Root[]; } /** - * Represents a root directory or file that the server can operate on. - */ + * Represents a root directory or file that the server can operate on. + */ export interface Root { /** * The URI identifying the root. This *must* start with file:// for now. @@ -1077,10 +1212,10 @@ export namespace MCP { } /** - * A notification from the client to the server, informing it that the list of roots has changed. - * This notification should be sent whenever the client adds, removes, or modifies any root. - * The server should then request an updated list of roots using the ListRootsRequest. - */ + * A notification from the client to the server, informing it that the list of roots has changed. + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + */ export interface RootsListChangedNotification extends Notification { method: "notifications/roots/list_changed"; } diff --git a/code/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/code/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index 8f4cbb70535..8fc67844c70 100644 --- a/code/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/code/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'; @@ -25,9 +25,11 @@ import { TestLoggerService, TestStorageService } from '../../../../test/common/w import { McpRegistry } from '../../common/mcpRegistry.js'; import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; import { McpServerConnection } from '../../common/mcpServerConnection.js'; -import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportStdio, 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; @@ -122,14 +124,17 @@ suite('Workbench - MCP - Registry', () => { 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()], @@ -180,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); @@ -239,6 +259,47 @@ suite('Workbench - MCP - Registry', () => { connection3.dispose(); }); + test('resolveConnection uses user-provided launch configuration', async () => { + // Create a collection with custom launch resolver + const customCollection: McpCollectionDefinition = { + ...testCollection, + resolveServerLanch: async (def) => { + return { + ...(def.launch as McpServerTransportStdio), + env: { CUSTOM_ENV: 'value' }, + }; + } + }; + + // Create a definition with variable replacement + const definition: McpServerDefinition = { + ...baseDefinition, + variableReplacement: { + section: 'mcp', + target: ConfigurationTarget.WORKSPACE, + } + }; + + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + testCollection.serverDefinitions.set([definition], undefined); + store.add(registry.registerCollection(customCollection)); + + // Resolve connection should use the custom launch configuration + const connection = await registry.resolveConnection({ + collectionRef: customCollection, + definitionRef: definition, + logger + }) as McpServerConnection; + + assert.ok(connection); + + // Verify the launch configuration passed to _replaceVariablesInLaunch was the custom one + assert.deepStrictEqual((connection.launchDefinition as McpServerTransportStdio).env, { CUSTOM_ENV: 'value' }); + + connection.dispose(); + }); + suite('Trust Management', () => { setup(() => { const delegate = new TestMcpHostDelegate(); @@ -553,6 +614,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/code/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 81e0dce9de2..f0c63879e6b 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -22,6 +22,10 @@ import { MergeEditor } from '../view/mergeEditor.js'; import { MergeEditorViewModel } from '../view/viewModel.js'; import { ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMergeEditorShowBaseAtTop, ctxMergeEditorShowNonConflictingChanges, StorageCloseWithConflicts } from '../../common/mergeEditor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { transaction } from '../../../../../base/common/observable.js'; +import { ModifiedBaseRangeStateKind } from '../model/modifiedBaseRange.js'; +import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; abstract class MergeEditorAction extends Action2 { constructor(desc: Readonly) { @@ -513,7 +517,7 @@ export class AcceptAllInput1 extends MergeEditorAction { super({ id: 'merge.acceptAllInput1', category: mergeEditorCategory, - title: localize2('merge.acceptAllInput1', "Accept All Changes from Left"), + title: localize2('merge.acceptAllInput1', "Accept All Incoming Changes from Left"), f1: true, precondition: ctxIsMergeEditor, menu: { id: MenuId.MergeInput1Toolbar, group: 'primary' }, @@ -531,7 +535,7 @@ export class AcceptAllInput2 extends MergeEditorAction { super({ id: 'merge.acceptAllInput2', category: mergeEditorCategory, - title: localize2('merge.acceptAllInput2', "Accept All Changes from Right"), + title: localize2('merge.acceptAllInput2', "Accept All Current Changes from Right"), f1: true, precondition: ctxIsMergeEditor, menu: { id: MenuId.MergeInput2Toolbar, group: 'primary' }, @@ -577,6 +581,41 @@ export class ResetCloseWithConflictsChoice extends Action2 { } } +export class AcceptAllCombination extends MergeEditorAction2 { + constructor() { + super({ + id: 'mergeEditor.acceptAllCombination', + category: mergeEditorCategory, + title: localize2('mergeEditor.acceptAllCombination', "Accept All Combination"), + f1: true, + }); + } + + override runWithMergeEditor(context: MergeEditorAction2Args, accessor: ServicesAccessor, ...args: any[]) { + const { viewModel } = context; + const modifiedBaseRanges = viewModel.model.modifiedBaseRanges.get(); + const model = viewModel.model; + transaction((tx) => { + for (const m of modifiedBaseRanges) { + const state = model.getState(m).get(); + if (state.kind !== ModifiedBaseRangeStateKind.unrecognized && !state.isInputIncluded(1) && (!state.isInputIncluded(2) || !viewModel.shouldUseAppendInsteadOfAccept.get()) && m.canBeCombined) { + model.setState( + m, + state + .withInputValue(1, true) + .withInputValue(2, true, true), + true, + tx + ); + model.telemetry.reportSmartCombinationInvoked(state.includesInput(2)); + } + } + }); + return { success: true }; + + } +} + // this is an API command export class AcceptMerge extends MergeEditorAction2 { constructor() { @@ -584,8 +623,15 @@ export class AcceptMerge extends MergeEditorAction2 { id: 'mergeEditor.acceptMerge', category: mergeEditorCategory, title: localize2('mergeEditor.acceptMerge', "Complete Merge"), - f1: false, - precondition: ctxIsMergeEditor + f1: true, + precondition: ctxIsMergeEditor, + keybinding: [ + { + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib, + when: ctxIsMergeEditor, + } + ] }); } @@ -615,3 +661,34 @@ export class AcceptMerge extends MergeEditorAction2 { }; } } + +export class ToggleBetweenInputs extends MergeEditorAction2 { + constructor() { + super({ + id: 'mergeEditor.toggleBetweenInputs', + category: mergeEditorCategory, + title: localize2('mergeEditor.toggleBetweenInputs', "Toggle Between Merge Editor Inputs"), + f1: true, + precondition: ctxIsMergeEditor, + keybinding: [ + { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyT, + // Override reopen closed editor + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ctxIsMergeEditor, + } + ] + }); + } + + override runWithMergeEditor({ viewModel }: MergeEditorAction2Args, accessor: ServicesAccessor) { + const input1IsFocused = viewModel.inputCodeEditorView1.editor.hasWidgetFocus(); + + // Toggle focus between inputs + if (input1IsFocused) { + viewModel.inputCodeEditorView2.editor.focus(); + } else { + viewModel.inputCodeEditorView1.editor.focus(); + } + } +} diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index bbf7bf34479..6399b030b16 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -15,7 +15,8 @@ import { AcceptAllInput1, AcceptAllInput2, AcceptMerge, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ShowHideTopBase, ShowHideCenterBase, ShowHideBase, - ShowNonConflictingChanges, ToggleActiveConflictInput1, ToggleActiveConflictInput2, ResetCloseWithConflictsChoice + ShowNonConflictingChanges, ToggleActiveConflictInput1, ToggleActiveConflictInput2, ResetCloseWithConflictsChoice, + AcceptAllCombination, ToggleBetweenInputs } from './commands/commands.js'; import { MergeEditorCopyContentsToJSON, MergeEditorLoadContentsFromFolder, MergeEditorSaveContentsToFolder } from './commands/devCommands.js'; import { MergeEditorInput } from './mergeEditorInput.js'; @@ -86,6 +87,9 @@ registerAction2(ResetToBaseAndAutoMergeCommand); registerAction2(AcceptMerge); registerAction2(ResetCloseWithConflictsChoice); +registerAction2(AcceptAllCombination); + +registerAction2(ToggleBetweenInputs); // Dev Commands registerAction2(MergeEditorCopyContentsToJSON); diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts index 2ead17bee71..cfa00747647 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts @@ -28,7 +28,9 @@ export class MergeEditorAccessibilityHelpProvider implements IAccessibleViewImpl const content = [ localize('msg1', "You are in a merge editor."), localize('msg2', "Navigate between merge conflicts using the commands Go to Next Unhandled Conflict{0} and Go to Previous Unhandled Conflict{1}.", '', ''), - localize('msg3', "Run the command Merge Editor: Accept All Changes from the Left{0} and Merge Editor: Accept All Changes from the Right{1}", '', ''), + localize('msg3', "Run the command Merge Editor: Accept All Incoming Changes from the Left{0} and Merge Editor: Accept All Current Changes from the Right{1}", '', ''), + localize('msg4', "Complete the Merge{0}.", ''), + localize('msg5', "Toggle between merge editor inputs, incoming and current changes {0}.", ''), ]; return new AccessibleContentProvider( diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 28217f06aa7..4a10caf524f 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -23,6 +23,9 @@ import { MergeEditorTelemetry } from './telemetry.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; import { ILanguageSupport, ITextFileSaveOptions, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { alert } from '../../../../base/browser/ui/aria/aria.js'; +import { MergeEditorType } from './view/viewModel.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class MergeEditorInputData { constructor( @@ -38,6 +41,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements private _inputModel?: IMergeEditorInputModel; + private _focusedEditor: MergeEditorType = 'result'; + override closeHandler: IEditorCloseHandler = { showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false, confirm: async (editors) => { @@ -65,6 +70,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, + @ILogService private readonly logService: ILogService, ) { super(result, undefined, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); } @@ -178,5 +184,30 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements this._inputModel?.model.setLanguageId(languageId, source); } + /** + * Updates the focused editor and triggers a name change event + */ + public updateFocusedEditor(editor: MergeEditorType): void { + if (this._focusedEditor !== editor) { + this._focusedEditor = editor; + this.logService.trace('alertFocusedEditor', editor); + alertFocusedEditor(editor); + } + } + // implement get/set encoding } + +function alertFocusedEditor(editor: MergeEditorType) { + switch (editor) { + case 'input1': + alert(localize('mergeEditor.input1', "Incoming, Left Input")); + break; + case 'input2': + alert(localize('mergeEditor.input2', "Current, Right Input")); + break; + case 'result': + alert(localize('mergeEditor.result', "Merge Result")); + break; + } +} diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts index eff77556a78..8c9f2b57bf4 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts @@ -40,7 +40,7 @@ export class LineRange { } public join(other: LineRange): LineRange { - return new LineRange(Math.min(this.startLineNumber, other.startLineNumber), Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive) - this.startLineNumber); + return LineRange.fromLineNumbers(Math.min(this.startLineNumber, other.startLineNumber), Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive)); } public get endLineNumberExclusive(): number { diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts index 3025d37f4be..13bd79f8584 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy, numberComparator } from '../../../../../base/common/arrays.js'; +import { compareBy, concatArrays, numberComparator } from '../../../../../base/common/arrays.js'; import { findLast } from '../../../../../base/common/arraysFind.js'; import { assertFn, checkAdjacentItems } from '../../../../../base/common/assert.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { concatArrays } from '../utils.js'; import { LineRangeEdit } from './editing.js'; import { LineRange } from './lineRange.js'; import { addLength, lengthBetweenPositions, rangeContainsPosition, rangeIsBeforeOrTouching } from './rangeUtils.js'; diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index 26c5f44afd1..8885b623ea1 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -77,15 +77,18 @@ export class MergeEditorModel extends EditorModel { this._register( autorunHandleChanges( { - handleChange: (ctx) => { - if (ctx.didChange(this.modifiedBaseRangeResultStates)) { - shouldRecomputeHandledFromAccepted = true; - } - return ctx.didChange(this.resultTextModelDiffs.diffs) - // Ignore non-text changes as we update the state directly - ? ctx.change === TextModelDiffChangeReason.textChange - : true; - }, + changeTracker: { + createChangeSummary: () => undefined, + handleChange: (ctx) => { + if (ctx.didChange(this.modifiedBaseRangeResultStates)) { + shouldRecomputeHandledFromAccepted = true; + } + return ctx.didChange(this.resultTextModelDiffs.diffs) + // Ignore non-text changes as we update the state directly + ? ctx.change === TextModelDiffChangeReason.textChange + : true; + }, + } }, (reader) => { /** @description Merge Editor Model: Recompute State From Result */ diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts index cd62b5944a1..3a4bd9618e9 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy, equals, numberComparator, tieBreakComparators } from '../../../../../base/common/arrays.js'; +import { compareBy, concatArrays, equals, numberComparator, tieBreakComparators } from '../../../../../base/common/arrays.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { splitLines } from '../../../../../base/common/strings.js'; import { Constants } from '../../../../../base/common/uint.js'; @@ -13,7 +13,6 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { LineRangeEdit, RangeEdit } from './editing.js'; import { LineRange } from './lineRange.js'; import { DetailedLineRangeMapping, MappingAlignment } from './mapping.js'; -import { concatArrays } from '../utils.js'; /** * Describes modifications in input 1 and input 2 for a specific range in base. diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index 308db886d5b..8c1698a42e8 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -77,10 +77,6 @@ export function* join( } } -export function concatArrays(...arrays: TArr): TArr[number][number][] { - return ([] as any[]).concat(...arrays); -} - export function elementAtOrUndefined(arr: T[], index: number): T | undefined { return arr[index]; } diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 666ad3f373e..6c6e6b5eaf3 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -218,6 +218,18 @@ export class MergeEditor extends AbstractTextEditor { }); this._sessionDisposables.add(viewModel); + // Track focus changes to update the editor name + this._sessionDisposables.add(autorun(reader => { + /** @description Update focused editor name based on focus */ + const focusedType = viewModel.focusedEditorType.read(reader); + + if (!(input instanceof MergeEditorInput)) { + return; + } + + input.updateFocusedEditor(focusedType || 'result'); + })); + // Set/unset context keys based on input this._ctxResultUri.set(inputModel.resultUri.toString()); this._ctxBaseUri.set(model.base.uri.toString()); @@ -226,12 +238,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 +260,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 +275,7 @@ export class MergeEditor extends AbstractTextEditor { )); }); } else { - store.add(this.setViewZones(reader, + viewZoneRegistrationStore.add(this.setViewZones(reader, viewModel, this.input1View.editor, input1ViewZoneAccessor, @@ -274,6 +293,9 @@ export class MergeEditor extends AbstractTextEditor { }); }); + this.inputResultView.editor.setScrollTop(resultScrollTop, ScrollType.Smooth); + + this.scrollSynchronizer.startSync(); this.scrollSynchronizer.updateScrolling(); })); diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts index 4d09d327f29..dec1af45d1b 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer.ts +++ b/code/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; - } - - 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 s = derivedWithStore((reader, store) => { + const baseView = this.baseView.read(reader); + const editors = [this.input1View, this.input2View, this.inputResultView, baseView].filter(isDefined); + + 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); + return { + update: () => { + alignScrolling(this.inputResultView, true, true); + } + }; + }).recomputeInitiallyAndOnChange(this._store); - 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); - } + this.updateScrolling = () => { + s.get().update(); + }; + } - 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); - } + public stopSync(): void { + this._isSyncing = false; + } - 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); + public startSync(): void { + this._isSyncing = true; + } - const baseInput2Mapping = new DocumentLineRangeMap(this.model.baseInput2Diffs.get(), -1); - this.synchronizeScrolling(baseView.editor, this.input2View.editor, baseInput2Mapping); - } + 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; + } - 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); - } - }) - )); - } - }) - ); + 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) { + private _synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: DocumentLineRangeMap | undefined) { if (!mapping) { return; } diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 74322dc65a4..b8158dc995a 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -116,6 +116,29 @@ export class MergeEditorViewModel extends Disposable { return undefined; }); + /** + * Returns an observable that tracks which editor type is currently focused + */ + public readonly focusedEditorType = derived(this, reader => { + const lastFocusedEditor = this.lastFocusedEditor.read(reader); + + if (!lastFocusedEditor.view) { + return undefined; + } + + if (lastFocusedEditor.view === this.inputCodeEditorView1) { + return 'input1'; + } else if (lastFocusedEditor.view === this.inputCodeEditorView2) { + return 'input2'; + } else if (lastFocusedEditor.view === this.resultCodeEditorView) { + return 'result'; + } else if (lastFocusedEditor.view === this.baseCodeEditorView.read(reader)) { + return 'base'; + } + + return undefined; + }); + public readonly selectionInBase = derived(this, reader => { const sourceEditor = this.lastFocusedEditor.read(reader).view; if (!sourceEditor) { @@ -343,3 +366,5 @@ interface IAttachedHistoryElement { undo(): void; redo(): void; } + +export type MergeEditorType = 'input1' | 'input2' | 'result' | 'base'; diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index 584fc890a92..dfd645ce241 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -15,6 +15,7 @@ import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { Event } from '../../../../../../base/common/event.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { autorun } from '../../../../../../base/common/observable.js'; export class CellDiagnostics extends Disposable implements INotebookEditorContribution { @@ -121,6 +122,11 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri disposables.push(toDisposable(() => this.markerService.changeOne(CellDiagnostics.ID, cell.uri, []))); cell.executionErrorDiagnostic.set(metadata.error, undefined); disposables.push(toDisposable(() => cell.executionErrorDiagnostic.set(undefined, undefined))); + disposables.push(autorun((r) => { + if (!cell.executionErrorDiagnostic.read(r)) { + this.clear(cellHandle); + } + })); disposables.push(cell.model.onDidChangeOutputs(() => { if (cell.model.outputs.length === 0) { this.clear(cellHandle); diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts new file mode 100644 index 00000000000..69f25f754e6 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { normalizeDriveLetter } from '../../../../../../base/common/labels.js'; +import { basenameOrAuthority } from '../../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { INotebookOutputVariableEntry } from '../../../../chat/common/chatModel.js'; +import { CellUri } from '../../../common/notebookCommon.js'; +import { ICellOutputViewModel, INotebookEditor } from '../../notebookBrowser.js'; + +export const NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST = [ + 'text/plain', + 'text/html', + 'application/vnd.code.notebook.error', + 'application/vnd.code.notebook.stdout', + 'application/x.notebook.stdout', + 'application/x.notebook.stream', + 'application/vnd.code.notebook.stderr', + 'application/x.notebook.stderr', + 'image/png', + 'image/jpeg', + 'image/svg', +]; + +export function createNotebookOutputVariableEntry(outputViewModel: ICellOutputViewModel, mimeType: string, notebookEditor: INotebookEditor): INotebookOutputVariableEntry | undefined { + + // get the cell index + const cellFromViewModelHandle = outputViewModel.cellViewModel.handle; + const notebookModel = notebookEditor.textModel; + const cell = notebookEditor.getCellByHandle(cellFromViewModelHandle); + if (!cell || cell.outputsViewModels.length === 0 || !notebookModel) { + return; + } + // uri of the cell + const notebookUri = notebookModel.uri; + const cellUri = cell.uri; + const cellIndex = notebookModel.cells.indexOf(cell.model); + + // get the output index + const outputId = outputViewModel?.model.outputId; + let outputIndex: number = 0; + if (outputId !== undefined) { + // find the output index + outputIndex = cell.outputsViewModels.findIndex(output => { + return output.model.outputId === outputId; + }); + } + + // construct the URI using the cell uri and output index + const outputCellUri = CellUri.generateCellOutputUriWithIndex(notebookUri, cellUri, outputIndex); + const fileName = normalizeDriveLetter(basenameOrAuthority(notebookUri)); + + const l: INotebookOutputVariableEntry = { + value: outputCellUri, + id: outputCellUri.toString(), + name: localize('notebookOutputCellLabel', "{0} • Cell {1} • Output {2}", fileName, `${cellIndex + 1}`, `${outputIndex + 1}`), + icon: mimeType === 'application/vnd.code.notebook.error' ? ThemeIcon.fromId('error') : undefined, + kind: 'notebookOutput', + outputIndex, + mimeType + }; + + return l; +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index 7df058a6901..0870ac79d1b 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -6,64 +6,41 @@ import { Schemas } from '../../../../../../base/common/network.js'; import { ICodeEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../../../editor/browser/editorExtensions.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; -import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; -import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; -import { IProductService } from '../../../../../../platform/product/common/productService.js'; -import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { EmptyTextEditorHintContribution, IEmptyTextEditorHintOptions } from '../../../../codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.js'; +import { EmptyTextEditorHintContribution } from '../../../../codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.js'; import { IInlineChatSessionService } from '../../../../inlineChat/browser/inlineChatSessionService.js'; import { getNotebookEditorFromEditorPane } from '../../notebookBrowser.js'; -import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribution { public static readonly CONTRIB_ID = 'notebook.editor.contrib.emptyCellEditorHint'; constructor( editor: ICodeEditor, @IEditorService private readonly _editorService: IEditorService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService, - @ICommandService commandService: ICommandService, @IConfigurationService configurationService: IConfigurationService, - @IHoverService hoverService: IHoverService, - @IKeybindingService keybindingService: IKeybindingService, @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @IChatAgentService chatAgentService: IChatAgentService, - @ITelemetryService telemetryService: ITelemetryService, - @IProductService productService: IProductService, - @IContextMenuService contextMenuService: IContextMenuService + @IInstantiationService instantiationService: IInstantiationService ) { super( editor, - editorGroupsService, - commandService, configurationService, - hoverService, - keybindingService, inlineChatSessionService, chatAgentService, - telemetryService, - productService, - contextMenuService + instantiationService ); const activeEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); - if (!activeEditor) { return; } - this.toDispose.push(activeEditor.onDidChangeActiveCell(() => this.update())); - } - - protected override _getOptions(): IEmptyTextEditorHintOptions { - return { clickable: false }; + this._register(activeEditor.onDidChangeActiveCell(() => this.update())); } - protected override _shouldRenderHint(): boolean { + protected override shouldRenderHint(): boolean { const model = this.editor.getModel(); if (!model) { return false; @@ -79,7 +56,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu return false; } - const shouldRenderHint = super._shouldRenderHint(); + const shouldRenderHint = super.shouldRenderHint(); if (!shouldRenderHint) { return false; } diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index f2de65d5bdc..64a78fe594a 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -24,6 +24,7 @@ import { Event } from '../../../../../../base/common/event.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { isSafari } from '../../../../../../base/common/platform.js'; +import { IHistory } from '../../../../../../base/common/history.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { FindReplaceState, FindReplaceStateChangedEvent } from '../../../../../../editor/contrib/find/browser/findState.js'; @@ -35,6 +36,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../../../../../platform/contextview/browser/contextView.js'; import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from '../../../../../../platform/history/browser/contextScopedHistoryWidget.js'; + import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; @@ -290,7 +292,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private readonly _innerFindDomNode: HTMLElement; private readonly _focusTracker: dom.IFocusTracker; private readonly _findInputFocusTracker: dom.IFocusTracker; - private readonly _updateHistoryDelayer: Delayer; + private readonly _updateFindHistoryDelayer: Delayer; protected readonly _matchesCount!: HTMLElement; private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; @@ -299,6 +301,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private readonly _innerReplaceDomNode!: HTMLElement; private _toggleReplaceBtn!: SimpleButton; private readonly _replaceInputFocusTracker!: dom.IFocusTracker; + private readonly _updateReplaceHistoryDelayer: Delayer; protected _replaceBtn!: SimpleButton; protected _replaceAllBtn!: SimpleButton; @@ -327,6 +330,8 @@ export abstract class SimpleFindReplaceWidget extends Widget { @IHoverService hoverService: IHoverService, protected readonly _state: FindReplaceState = new FindReplaceState(), protected readonly _notebookEditor: INotebookEditor, + private readonly _findWidgetSearchHistory: IHistory | undefined, + private readonly _replaceWidgetHistory: IHistory | undefined, ) { super(); @@ -339,6 +344,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { codeOutput: boolean; }>(NotebookSetting.findFilters) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; + const findHistoryConfig = this._configurationService.getValue<'never' | 'workspace'>('editor.find.history'); + const replaceHistoryConfig = this._configurationService.getValue<'never' | 'workspace'>('editor.find.replaceHistory'); + this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, { findScopeType: NotebookFindScopeType.None }); this._state.change({ filters: this._filters }, false); @@ -414,17 +422,18 @@ export abstract class SimpleFindReplaceWidget extends Widget { flexibleWidth: true, showCommonFindToggles: true, inputBoxStyles: defaultInputBoxStyles, - toggleStyles: defaultToggleStyles + toggleStyles: defaultToggleStyles, + history: findHistoryConfig === 'workspace' ? this._findWidgetSearchHistory : new Set([]), } )); // Find History with update delayer - this._updateHistoryDelayer = new Delayer(500); + this._updateFindHistoryDelayer = new Delayer(500); this.oninput(this._findInput.domNode, (e) => { this.foundMatch = this.onInputChanged(); this.updateButtons(this.foundMatch); - this._delayedUpdateHistory(); + this._delayedUpdateFindHistory(); }); this._register(this._findInput.inputBox.onDidChange(() => { @@ -568,7 +577,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: new Set([]), + history: replaceHistoryConfig === 'workspace' ? this._replaceWidgetHistory : new Set([]), inputBoxStyles: defaultInputBoxStyles, toggleStyles: defaultToggleStyles }, contextKeyService, false)); @@ -577,6 +586,13 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._register(this._replaceInputFocusTracker.onDidFocus(this.onReplaceInputFocusTrackerFocus.bind(this))); this._register(this._replaceInputFocusTracker.onDidBlur(this.onReplaceInputFocusTrackerBlur.bind(this))); + // Replace History with update delayer + this._updateReplaceHistoryDelayer = new Delayer(500); + + this.oninput(this._replaceInput.domNode, (e) => { + this._delayedUpdateReplaceHistory(); + }); + this._register(this._replaceInput.inputBox.onDidChange(() => { this._state.change({ replaceString: this._replaceInput.getValue() }, true); })); @@ -867,14 +883,22 @@ export abstract class SimpleFindReplaceWidget extends Widget { } } - protected _delayedUpdateHistory() { - this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)); + protected _delayedUpdateFindHistory() { + this._updateFindHistoryDelayer.trigger(this._updateFindHistory.bind(this)); } - protected _updateHistory() { + protected _updateFindHistory() { this._findInput.inputBox.addToHistory(); } + protected _delayedUpdateReplaceHistory() { + this._updateReplaceHistoryDelayer.trigger(this._updateReplaceHistory.bind(this)); + } + + protected _updateReplaceHistory() { + this._replaceInput.inputBox.addToHistory(); + } + protected _getRegexValue(): boolean { return this._findInput.getRegex(); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index 6c57739806f..bb0c49f4bba 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -15,12 +15,15 @@ import { FindMatch } from '../../../../../../editor/common/model.js'; import { MATCHES_LIMIT } from '../../../../../../editor/contrib/find/browser/findModel.js'; import { FindReplaceState } from '../../../../../../editor/contrib/find/browser/findState.js'; import { NLS_MATCHES_LOCATION, NLS_NO_RESULTS } from '../../../../../../editor/contrib/find/browser/findWidget.js'; +import { FindWidgetSearchHistory } from '../../../../../../editor/contrib/find/browser/findWidgetSearchHistory.js'; +import { ReplaceWidgetHistory } from '../../../../../../editor/contrib/find/browser/replaceWidgetHistory.js'; import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; import { NotebookFindFilters } from './findFilters.js'; import { FindModel } from './findModel.js'; import { SimpleFindReplaceWidget } from './notebookFindReplaceWidget.js'; @@ -91,8 +94,12 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi @IContextMenuService contextMenuService: IContextMenuService, @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, ) { - super(contextViewService, contextKeyService, configurationService, contextMenuService, instantiationService, hoverService, new FindReplaceState(), _notebookEditor); + const findSearchHistory = FindWidgetSearchHistory.getOrCreate(storageService); + const replaceHistory = ReplaceWidgetHistory.getOrCreate(storageService); + + super(contextViewService, contextKeyService, configurationService, contextMenuService, instantiationService, hoverService, new FindReplaceState(), _notebookEditor, findSearchHistory, replaceHistory); this._findModel = new FindModel(this._notebookEditor, this._state, this._configurationService); DOM.append(this._notebookEditor.getDomNode(), this.getDomNode()); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index db11bd0fe77..099b3639901 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -26,7 +26,7 @@ registerAction2(class ShowAllOutputsAction extends Action2 { constructor() { super({ id: 'notebook.cellOuput.showEmptyOutputs', - title: localize('notebookActions.showAllOutput', "Show empty outputs"), + title: localize('notebookActions.showAllOutput', "Show Empty Outputs"), menu: { id: MenuId.NotebookOutputToolbar, when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS) @@ -181,3 +181,82 @@ registerAction2(class OpenCellOutputInEditorAction extends Action2 { } } }); + +export const OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID = 'notebook.cellOutput.openInOutputPreview'; + +registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 { + constructor() { + super({ + id: OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID, + title: localize('notebookActions.openOutputInNotebookOutputEditor', "Open in Output Preview"), + menu: { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, ContextKeyExpr.equals('config.notebook.output.openInPreviewEditor.enabled', true)) + }, + f1: false, + category: NOTEBOOK_ACTIONS_CATEGORY, + }); + } + + private getNotebookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { + if (outputContext && 'notebookEditor' in outputContext) { + return outputContext.notebookEditor; + } + return getNotebookEditorFromEditorPane(editorService.activeEditorPane); + } + + async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { + const notebookEditor = this.getNotebookEditor(accessor.get(IEditorService), outputContext); + if (!notebookEditor) { + return; + } + + let outputViewModel: ICellOutputViewModel | undefined; + if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { + outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); + } else if (outputContext && 'outputViewModel' in outputContext) { + outputViewModel = outputContext.outputViewModel; + } + + if (!outputViewModel) { + return; + } + + const genericCellViewModel = outputViewModel.cellViewModel; + if (!genericCellViewModel) { + return; + } + + // get cell index + const cellViewModel = notebookEditor.getCellByHandle(genericCellViewModel.handle); + if (!cellViewModel) { + return; + } + const cellIndex = notebookEditor.getCellIndex(cellViewModel); + if (cellIndex === undefined) { + return; + } + + // get output index + const outputIndex = genericCellViewModel.outputsViewModels.indexOf(outputViewModel); + if (outputIndex === -1) { + return; + } + + if (!notebookEditor.textModel) { + return; + } + + // craft rich output URI to pass data to the notebook output editor/viewer + const outputURI = CellUri.generateOutputEditorUri( + notebookEditor.textModel.uri, + cellViewModel.id, + cellIndex, + outputViewModel.model.outputId, + outputIndex, + ); + + const openerService = accessor.get(IOpenerService); + openerService.open(outputURI, { openToSide: true }); + } +}); diff --git a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index 1875c7304c3..6993def3c8a 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -19,37 +19,26 @@ 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'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; import { ChatContextKeys } from '../../../../chat/common/chatContextKeys.js'; -import { IBaseChatRequestVariableEntry } from '../../../../chat/common/chatModel.js'; import { chatVariableLeader } from '../../../../chat/common/chatParserTypes.js'; import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../../common/notebookContextKeys.js'; import { INotebookKernelService } from '../../../common/notebookKernelService.js'; -import { getNotebookEditorFromEditorPane, ICellOutputViewModel, INotebookEditor, ICellViewModel } from '../../notebookBrowser.js'; +import { getNotebookEditorFromEditorPane, ICellOutputViewModel, INotebookEditor } from '../../notebookBrowser.js'; import * as icons from '../../notebookIcons.js'; import { getOutputViewModelFromId } from '../cellOutputActions.js'; import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from '../coreActions.js'; -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'; +import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../contrib/chat/notebookChatUtils.js'; const NotebookKernelVariableKey = 'kernelVariable'; -const NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST = ['text/plain', 'text/html', - 'application/vnd.code.notebook.error', - 'application/vnd.code.notebook.stdout', - 'application/x.notebook.stdout', - 'application/x.notebook.stream', - 'application/vnd.code.notebook.stderr', - 'application/x.notebook.stderr', - 'image/png', - 'image/jpeg', - 'image/svg', -]; class NotebookChatContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.notebookChatContribution'; @@ -245,6 +234,7 @@ export class SelectAndInsertKernelVariableAction extends Action2 { name: variableName, value: variableName, icon: codiconsLibrary.variable, + kind: 'generic' }); } } @@ -259,7 +249,8 @@ registerAction2(class CopyCellOutputAction extends Action2 { menu: { id: MenuId.NotebookOutputToolbar, when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, ContextKeyExpr.in(NOTEBOOK_CELL_OUTPUT_MIMETYPE.key, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT.key)), - order: 10 + order: 10, + group: 'notebook_chat_actions' }, category: NOTEBOOK_ACTIONS_CATEGORY, icon: icons.copyIcon, @@ -276,6 +267,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; @@ -321,49 +313,13 @@ registerAction2(class CopyCellOutputAction extends Action2 { } if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) { - // get the cell index - const cellFromViewModelHandle = outputViewModel.cellViewModel.handle; - const cell: ICellViewModel | undefined = notebookEditor.getCellByHandle(cellFromViewModelHandle); - if (!cell) { + const entry = createNotebookOutputVariableEntry(outputViewModel, mimeType, notebookEditor); + if (!entry) { return; } - // uri of the cell - const cellUri = cell.uri; - // get the output index - const outputId = outputViewModel?.model.outputId; - let outputIndex: number = 0; - if (outputId !== undefined) { - // find the output index - - outputIndex = cell.outputsViewModels.findIndex(output => { - return output.model.outputId === outputId; - }); - - - } - // get URI of notebook - let notebookUri = notebookEditor.textModel?.uri; - if (!notebookUri) { - // if the notebook is not found, try to parse the cell uri - const parsedCellUri = CellUri.parse(cellUri); - notebookUri = parsedCellUri?.notebook; - if (!notebookUri) { - return; - } - } - // construct the URI using the cell uri and output index - const outputCellUri = CellUri.generateCellOutputUriWithIndex(notebookUri, cellUri, outputIndex); - - - - const l: IBaseChatRequestVariableEntry = { - value: outputCellUri, - id: outputCellUri.toString(), - name: outputCellUri.toString(), - isFile: true, - }; - widget.attachmentModel.addContext(l); + widget.attachmentModel.addContext(entry); + (await showChatView(viewService))?.focusInput(); } } diff --git a/code/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts b/code/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts index 540f3080980..9666f98016a 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts @@ -220,7 +220,7 @@ export class NotebookDeletedCellWidget extends Disposable { if (this._toolbarOptions) { const toolbar = document.createElement('div'); - toolbar.className = this._toolbarOptions?.className; + toolbar.className = this._toolbarOptions.className; rootContainer.appendChild(toolbar); const scopedInstaService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this._notebookEditor.scopedContextKeyService]))); diff --git a/code/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts b/code/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts index c6d7549eb6e..1f6a760cdff 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts @@ -12,7 +12,7 @@ import { IEditorOpenContext } from '../../../../common/editor.js'; import { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IEditorOptions as ICodeEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; @@ -46,7 +46,7 @@ export class NotebookMultiTextDiffEditor extends EditorPane { static readonly ID: string = NOTEBOOK_MULTI_DIFF_EDITOR_ID; private _fontInfo: FontInfo | undefined; protected _scopeContextKeyService!: IContextKeyService; - private readonly modelSpecificResources = this._register(new DisposableStore()); + private readonly modelSpecificResources: DisposableStore; private _model?: INotebookDiffEditorModel; private viewModel?: NotebookDiffViewModel; private widgetViewModel?: MultiDiffEditorViewModel; @@ -57,9 +57,9 @@ export class NotebookMultiTextDiffEditor extends EditorPane { get notebookOptions() { return this._notebookOptions; } - private readonly ctxAllCollapsed = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_CELLS_COLLAPSED.key, false); - private readonly ctxHasUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS.key, false); - private readonly ctxHiddenUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN.key, true); + private readonly ctxAllCollapsed: IContextKey; + private readonly ctxHasUnchangedCells: IContextKey; + private readonly ctxHiddenUnchangedCells: IContextKey; constructor( group: IEditorGroup, @@ -73,6 +73,10 @@ export class NotebookMultiTextDiffEditor extends EditorPane { @INotebookService private readonly notebookService: INotebookService, ) { super(NotebookMultiTextDiffEditor.ID, group, telemetryService, themeService, storageService); + this.modelSpecificResources = this._register(new DisposableStore()); + this.ctxAllCollapsed = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_CELLS_COLLAPSED.key, false); + this.ctxHasUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_HAS_UNCHANGED_CELLS.key, false); + this.ctxHiddenUnchangedCells = this._parentContextKeyService.createKey(NOTEBOOK_DIFF_UNCHANGED_CELLS_HIDDEN.key, true); this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); this._register(this._notebookOptions); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 10a50d99cdf..d1f2af83275 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -132,6 +132,8 @@ import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInpu import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js'; import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js'; import { getFormattedNotebookMetadataJSON } from '../common/model/notebookMetadataTextModel.js'; +import { NotebookOutputEditor } from './outputEditor/notebookOutputEditor.js'; +import { NotebookOutputEditorInput } from './outputEditor/notebookOutputEditorInput.js'; /*--------------------------------------------------------------------------------------------- */ @@ -157,6 +159,17 @@ Registry.as(EditorExtensions.EditorPane).registerEditorPane ] ); +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + NotebookOutputEditor, + NotebookOutputEditor.ID, + 'Notebook Output Editor' + ), + [ + new SyncDescriptor(NotebookOutputEditorInput) + ] +); + Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( NotebookMultiTextDiffEditor, @@ -239,6 +252,32 @@ class NotebookEditorSerializer implements IEditorSerializer { } } +export type SerializedNotebookOutputEditorData = { notebookUri: URI; cellIndex: number; outputIndex: number }; +class NotebookOutputEditorSerializer implements IEditorSerializer { + canSerialize(input: EditorInput): boolean { + return input.typeId === NotebookOutputEditorInput.ID; + } + serialize(input: EditorInput): string | undefined { + assertType(input instanceof NotebookOutputEditorInput); + + const data = input.getSerializedData(); // in case of cell movement etc get latest indices + if (!data) { + return undefined; + } + + return JSON.stringify(data); + } + deserialize(instantiationService: IInstantiationService, raw: string): EditorInput | undefined { + const data = parse(raw); + if (!data) { + return undefined; + } + + const input = instantiationService.createInstance(NotebookOutputEditorInput, data.notebookUri, data.cellIndex, undefined, data.outputIndex); + return input; + } +} + Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( NotebookEditorInput.ID, NotebookEditorSerializer @@ -249,6 +288,11 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit NotebookDiffEditorSerializer ); +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + NotebookOutputEditorInput.ID, + NotebookOutputEditorSerializer +); + export class NotebookContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.notebook'; @@ -1046,6 +1090,12 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['notebookLayout'] }, + // [NotebookSetting.openOutputInPreviewEditor]: { + // description: nls.localize('notebook.output.openInPreviewEditor.description', "Controls whether or not the action to open a cell output in a preview editor is enabled. This action can be used via the cell output menu."), + // type: 'boolean', + // default: false, + // tags: ['preview'] + // }, [NotebookSetting.showFoldingControls]: { description: nls.localize('notebook.showFoldingControls.description', "Controls when the Markdown header folding arrow is shown."), type: 'string', diff --git a/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditor.ts b/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditor.ts new file mode 100644 index 00000000000..02ff76b4f0c --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditor.ts @@ -0,0 +1,385 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from '../../../../../base/browser/dom.js'; +import * as nls from '../../../../../nls.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { EditorPane } from '../../../../browser/parts/editor/editorPane.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; +import { IEditorOpenContext } from '../../../../common/editor.js'; +import { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; +import { IEditorResolverService, RegisteredEditorPriority } from '../../../../services/editor/common/editorResolverService.js'; +import { CellUri, NOTEBOOK_OUTPUT_EDITOR_ID } from '../../common/notebookCommon.js'; +import { INotebookService } from '../../common/notebookService.js'; +import { CellEditState, IBaseCellEditorOptions, ICellOutputViewModel, ICommonCellInfo, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorCreationOptions, RenderOutputType } from '../notebookBrowser.js'; +import { getDefaultNotebookCreationOptions } from '../notebookEditorWidget.js'; +import { NotebookOptions } from '../notebookOptions.js'; +import { BackLayerWebView, INotebookDelegateForWebview } from '../view/renderers/backLayerWebView.js'; +import { NotebookOutputEditorInput } from './notebookOutputEditorInput.js'; +import { BareFontInfo, FontInfo } from '../../../../../editor/common/config/fontInfo.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IEditorOptions as ICodeEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { FontMeasurements } from '../../../../../editor/browser/config/fontMeasurements.js'; +import { PixelRatio } from '../../../../../base/browser/pixelRatio.js'; +import { NotebookViewModel } from '../viewModel/notebookViewModelImpl.js'; +import { NotebookEventDispatcher } from '../viewModel/eventDispatcher.js'; +import { ViewContext } from '../viewModel/viewContext.js'; + +export class NoopCellEditorOptions extends Disposable implements IBaseCellEditorOptions { + private static fixedEditorOptions: ICodeEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + folding: true, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + lineNumbersMinChars: 3 + }; + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + private _value: ICodeEditorOptions; + + get value(): Readonly { + return this._value; + } + + constructor() { + super(); + this._value = Object.freeze({ + ...NoopCellEditorOptions.fixedEditorOptions, + padding: { top: 12, bottom: 12 }, + readOnly: true + }); + } +} + +export class NotebookOutputEditor extends EditorPane implements INotebookDelegateForWebview { + + static readonly ID: string = NOTEBOOK_OUTPUT_EDITOR_ID; + + creationOptions: INotebookEditorCreationOptions = getDefaultNotebookCreationOptions(); + + private _rootElement!: HTMLElement; + private _outputWebview: BackLayerWebView | null = null; + + private _fontInfo: FontInfo | undefined; + + private _notebookOptions: NotebookOptions; + private _notebookViewModel: NotebookViewModel | undefined; + + private _isDisposed: boolean = false; + get isDisposed() { + return this._isDisposed; + } + + constructor( + group: IEditorGroup, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotebookService private readonly notebookService: INotebookService, + + ) { + super(NotebookOutputEditor.ID, group, telemetryService, themeService, storageService); + this._notebookOptions = this.instantiationService.createInstance(NotebookOptions, this.window, false, undefined); + this._register(this._notebookOptions); + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.notebook-output-editor')); + } + + private get fontInfo() { + if (!this._fontInfo) { + this._fontInfo = this.createFontInfo(); + } + + return this._fontInfo; + } + + private createFontInfo() { + const editorOptions = this.configurationService.getValue('editor'); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); + } + + private async _createOriginalWebview(id: string, viewType: string, resource: URI): Promise { + this._outputWebview?.dispose(); + + this._outputWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, viewType, resource, { + ...this._notebookOptions.computeDiffWebviewOptions(), + fontFamily: this._generateFontFamily() + }, undefined) as BackLayerWebView; + + // attach the webview container to the DOM tree first + DOM.append(this._rootElement, this._outputWebview.element); + + this._outputWebview.createWebview(this.window); + this._outputWebview.element.style.width = `calc(100% - 16px)`; + this._outputWebview.element.style.left = `16px`; + + } + + private _generateFontFamily(): string { + return this.fontInfo.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; + } + + override getTitle(): string { + if (this.input) { + return this.input.getName(); + } + + return nls.localize('notebookOutputEditor', "Notebook Output Editor"); + } + + override async setInput(input: NotebookOutputEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + const model = await input.resolve(); + if (!model) { + throw new Error('Invalid notebook output editor input'); + } + + const resolvedNotebookEditorModel = model.resolvedNotebookEditorModel; + + await this._createOriginalWebview(generateUuid(), resolvedNotebookEditorModel.viewType, URI.from({ scheme: Schemas.vscodeNotebookCellOutput, path: '', query: 'openIn=notebookOutputEditor' })); + + const notebookTextModel = resolvedNotebookEditorModel.notebook; + const eventDispatcher = this._register(new NotebookEventDispatcher()); + const editorOptions = this._register(new NoopCellEditorOptions()); + const viewContext = new ViewContext( + this._notebookOptions, + eventDispatcher, + _language => editorOptions + ); + + this._notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, notebookTextModel.viewType, notebookTextModel, viewContext, null, { isReadOnly: true }); + + const cellViewModel = this._notebookViewModel.getCellByHandle(model.cell.handle); + if (!cellViewModel) { + throw new Error('Invalid NotebookOutputEditorInput, no matching cell view model'); + } + + const cellOutputViewModel = cellViewModel.outputsViewModels.find(outputViewModel => outputViewModel.model.outputId === model.outputId); + if (!cellOutputViewModel) { + throw new Error('Invalid NotebookOutputEditorInput, no matching cell output view model'); + } + + let result: IInsetRenderOutput | undefined = undefined; + + const [mimeTypes, pick] = cellOutputViewModel.resolveMimeTypes(notebookTextModel, undefined); + const pickedMimeTypeRenderer = cellOutputViewModel.pickedMimeType || mimeTypes[pick]; + if (mimeTypes.length !== 0) { + const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: cellOutputViewModel, mimeType: pickedMimeTypeRenderer.mimeType } + : this._renderMissingRenderer(cellOutputViewModel, pickedMimeTypeRenderer.mimeType); + + } + + if (!result) { + throw new Error('No InsetRenderInfo for output'); + } + + const cellInfo: ICommonCellInfo = { + cellId: cellViewModel.id, + cellHandle: model.cell.handle, + cellUri: model.cell.uri, + }; + + this._outputWebview?.createOutput(cellInfo, result, 0, 0); + } + + private _renderMissingRenderer(viewModel: ICellOutputViewModel, preferredMimeType: string | undefined): IInsetRenderOutput { + if (!viewModel.model.outputs.length) { + return this._renderMessage(viewModel, nls.localize('empty', "Cell has no output")); + } + + if (!preferredMimeType) { + const mimeTypes = viewModel.model.outputs.map(op => op.mime); + const mimeTypesMessage = mimeTypes.join(', '); + return this._renderMessage(viewModel, nls.localize('noRenderer.2', "No renderer could be found for output. It has the following mimetypes: {0}", mimeTypesMessage)); + } + + return this._renderSearchForMimetype(viewModel, preferredMimeType); + } + + private _renderMessage(viewModel: ICellOutputViewModel, message: string): IInsetRenderOutput { + const el = DOM.$('p', undefined, message); + return { type: RenderOutputType.Html, source: viewModel, htmlContent: el.outerHTML }; + } + + private _renderSearchForMimetype(viewModel: ICellOutputViewModel, mimeType: string): IInsetRenderOutput { + const query = `@tag:notebookRenderer ${mimeType}`; + + const p = DOM.$('p', undefined, `No renderer could be found for mimetype "${mimeType}", but one might be available on the Marketplace.`); + const a = DOM.$('a', { href: `command:workbench.extensions.search?%22${query}%22`, class: 'monaco-button monaco-text-button', tabindex: 0, role: 'button', style: 'padding: 8px; text-decoration: none; color: rgb(255, 255, 255); background-color: rgb(14, 99, 156); max-width: 200px;' }, `Search Marketplace`); + + return { + type: RenderOutputType.Html, + source: viewModel, + htmlContent: p.outerHTML + a.outerHTML, + }; + } + + scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void { + DOM.scheduleAtNextAnimationFrame(this.window, () => { + this._outputWebview?.ackHeight([{ cellId: cellInfo.cellId, outputId, height }]); + }, 10); + } + + async focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): Promise { + + } + + async focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): Promise { + + } + + toggleNotebookCellSelection(cell: IGenericCellViewModel) { + throw new Error('Not implemented.'); + } + + getCellById(cellId: string): IGenericCellViewModel | undefined { + throw new Error('Not implemented'); + } + + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel { + return this._notebookViewModel?.getCellByHandle(cellInfo.cellHandle) as IGenericCellViewModel; + } + + layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { + + } + + setScrollTop(scrollTop: number): void { + + } + + triggerScroll(event: any): void { + + } + + getOutputRenderer(): any { + + } + + updateOutputHeight(cellInfo: ICommonCellInfo, output: ICellOutputViewModel, height: number, isInit: boolean, source?: string): void { + + } + + updateMarkupCellHeight(cellId: string, height: number, isInit: boolean): void { + + } + + setMarkupCellEditState(cellId: string, editState: CellEditState): void { + + } + + didResizeOutput(cellId: string): void { + + } + + didStartDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void { + + } + + didDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void { + + } + + didDropMarkupCell(cellId: string, event: { dragOffsetY: number; ctrlKey: boolean; altKey: boolean }): void { + + } + + didEndDragMarkupCell(cellId: string): void { + + } + + updatePerformanceMetadata(cellId: string, executionId: string, duration: number, rendererId: string): void { + + } + + didFocusOutputInputChange(inputFocused: boolean): void { + + } + + override dispose() { + this._isDisposed = true; + super.dispose(); + } +} + +export class NotebookOutputEditorContribution implements IWorkbenchContribution { + + static readonly ID = 'workbench.contribution.notebookOutputEditorContribution'; + + constructor( + @IEditorResolverService editorResolverService: IEditorResolverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService,) { + editorResolverService.registerEditor( + `${Schemas.vscodeNotebookCellOutput}:/**`, + { + id: 'notebookOutputEditor', + label: 'Notebook Output Editor', + priority: RegisteredEditorPriority.option + }, + { + canSupportResource: (resource: URI) => { + if (resource.scheme === Schemas.vscodeNotebookCellOutput) { + const params = new URLSearchParams(resource.query); + return params.get('openIn') === 'notebookOutputEditor'; + } + return false; + } + }, + { + createEditorInput: async ({ resource, options }) => { + const outputUriData = CellUri.parseCellOutputUri(resource); + if (!outputUriData || !outputUriData.notebook || outputUriData.cellIndex === undefined || outputUriData.outputIndex === undefined || !outputUriData.outputId) { + throw new Error('Invalid output uri for notebook output editor'); + } + + const notebookUri = this.uriIdentityService.asCanonicalUri(outputUriData.notebook); + const cellIndex = outputUriData.cellIndex; + const outputId = outputUriData.outputId; + const outputIndex = outputUriData.outputIndex; + + const editorInput = this.instantiationService.createInstance(NotebookOutputEditorInput, notebookUri, cellIndex, outputId, outputIndex); + return { + editor: editorInput, + options: options + }; + } + } + ); + } +} + +registerWorkbenchContribution2(NotebookOutputEditorContribution.ID, NotebookOutputEditorContribution, WorkbenchPhase.BlockRestore); diff --git a/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditorInput.ts b/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditorInput.ts new file mode 100644 index 00000000000..78a4f2e91a7 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditorInput.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from '../../../../../nls.js'; +import { IDisposable, IReference } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { EditorInputCapabilities } from '../../../../common/editor.js'; +import { EditorInput } from '../../../../common/editor/editorInput.js'; +import { IResolvedNotebookEditorModel } from '../../common/notebookCommon.js'; +import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; + + +class ResolvedNotebookOutputEditorInputModel implements IDisposable { + constructor( + readonly resolvedNotebookEditorModel: IResolvedNotebookEditorModel, + readonly notebookUri: URI, + readonly cell: NotebookCellTextModel, + readonly outputId: string, + ) { } + + dispose(): void { + this.resolvedNotebookEditorModel.dispose(); + } +} + +// TODO @Yoyokrazy -- future feat. for viewing static outputs -- encode mime + data +// export class NotebookOutputViewerInput extends EditorInput { +// static readonly ID: string = 'workbench.input.notebookOutputViewerInput'; +// } + +export class NotebookOutputEditorInput extends EditorInput { + static readonly ID: string = 'workbench.input.notebookOutputEditorInput'; + + private _notebookRef: IReference | undefined; + private readonly _notebookUri: URI; + + readonly cellIndex: number; + + public cellUri: URI | undefined; + + readonly outputIndex: number; + private outputId: string | undefined; + + constructor( + notebookUri: URI, + cellIndex: number, + outputId: string | undefined, + outputIndex: number, + @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService, + ) { + super(); + this._notebookUri = notebookUri; + + this.cellUri = undefined; + this.cellIndex = cellIndex; + + this.outputId = outputId; + this.outputIndex = outputIndex; + } + + override get typeId(): string { + return NotebookOutputEditorInput.ID; + } + + override async resolve(): Promise { + if (!this._notebookRef) { + this._notebookRef = await this.notebookEditorModelResolverService.resolve(this._notebookUri); + } + + const cell = this._notebookRef.object.notebook.cells[this.cellIndex]; + if (!cell) { + throw new Error('Cell not found'); + } + + this.cellUri = cell.uri; + + const resolvedOutputId = cell.outputs[this.outputIndex]?.outputId; + if (!resolvedOutputId) { + throw new Error('Output not found'); + } + + if (!this.outputId) { + this.outputId = resolvedOutputId; + } + + return new ResolvedNotebookOutputEditorInputModel( + this._notebookRef.object, + this._notebookUri, + cell, + resolvedOutputId, + ); + } + + public getSerializedData(): { notebookUri: URI; cellIndex: number; outputIndex: number } | undefined { + // need to translate from uris -> current indexes + // uris aren't deterministic across reloads, so indices are best option + + if (!this._notebookRef) { + return; + } + + const cellIndex = this._notebookRef.object.notebook.cells.findIndex(c => isEqual(c.uri, this.cellUri)); + const cell = this._notebookRef.object.notebook.cells[cellIndex]; + if (!cell) { + return; + } + + const outputIndex = cell.outputs.findIndex(o => o.outputId === this.outputId); + if (outputIndex === -1) { + return; + } + + return { + notebookUri: this._notebookUri, + cellIndex: cellIndex, + outputIndex: outputIndex, + }; + } + + override getName(): string { + return nls.localize('notebookOutputEditorInput', "Notebook Output Preview"); + } + + override get editorId(): string { + return 'notebookOutputEditor'; + } + + override get resource(): URI | undefined { + return; + } + + override get capabilities() { + return EditorInputCapabilities.Readonly; + } + + override dispose(): void { + super.dispose(); + } +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 84b710977da..20cd1c5aee0 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -194,7 +194,7 @@ class CellOutputElement extends Disposable { const notebookTextModel = this.notebookEditor.textModel; const [mimeTypes, pick] = this.output.resolveMimeTypes(notebookTextModel, this.notebookEditor.activeKernel?.preloadProvides); - + const currentMimeType = mimeTypes[pick]; if (!mimeTypes.find(mimeType => mimeType.isTrusted) || mimeTypes.length === 0) { this.viewCell.updateOutputHeight(index, 0, 'CellOutputElement#noMimeType'); return undefined; @@ -208,12 +208,12 @@ class CellOutputElement extends Disposable { const innerContainer = this._generateInnerOutputContainer(previousSibling, selectedPresentation); if (index === 0 || this.output.visible.get()) { - this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, currentMimeType, mimeTypes); } else { this._register(autorun((reader) => { const visible = reader.readObservable(this.output.visible); if (visible && !this.toolbarAttached) { - this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); + this._attachToolbar(innerContainer, notebookTextModel, this.notebookEditor.activeKernel, index, currentMimeType, mimeTypes); } else if (!visible) { this.toolbarDisposables.clear(); } @@ -292,7 +292,7 @@ class CellOutputElement extends Disposable { return true; } - private async _attachToolbar(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, index: number, mimeTypes: readonly IOrderedMimeType[]) { + private async _attachToolbar(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, index: number, currentMimeType: IOrderedMimeType, mimeTypes: readonly IOrderedMimeType[]) { const hasMultipleMimeTypes = mimeTypes.filter(mimeType => mimeType.isTrusted).length > 1; const isCopyEnabled = this.shouldEnableCopy(mimeTypes); if (index > 0 && !hasMultipleMimeTypes && !isCopyEnabled) { @@ -329,10 +329,8 @@ class CellOutputElement extends Disposable { const isFirstCellOutput = NOTEBOOK_CELL_IS_FIRST_OUTPUT.bindTo(menuContextKeyService); const cellOutputMimetype = NOTEBOOK_CELL_OUTPUT_MIMETYPE.bindTo(menuContextKeyService); isFirstCellOutput.set(index === 0); - if (mimeTypes[index]) { - cellOutputMimetype.set(mimeTypes[index].mimeType); - } - this.toolbarDisposables.add(autorun((reader) => { hasHiddenOutputs.set(reader.readObservable(this.cellOutputContainer.hasHiddenOutputs)); })); + cellOutputMimetype.set(currentMimeType.mimeType); + this.toolbarDisposables.add(autorun((r) => { hasHiddenOutputs.set(this.cellOutputContainer.hasHiddenOutputs.read(r)); })); const menu = this.toolbarDisposables.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, menuContextKeyService)); const updateMenuToolbar = () => { @@ -492,7 +490,7 @@ export class CellOutputContainer extends CellContentPart { hasHiddenOutputs = observableValue('hasHiddenOutputs', false); checkForHiddenOutputs() { - if (this._outputEntries.find(entry => { return entry.model.visible; })) { + if (this._outputEntries.find(entry => { return !entry.model.visible.get(); })) { this.hasHiddenOutputs.set(true, undefined); } else { this.hasHiddenOutputs.set(false, undefined); diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index b595e863285..5f1497f644f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -352,7 +352,6 @@ export class CodeCell extends Disposable { })); this._register(this.templateData.editor.onDidBlurEditorWidget(() => { - CodeActionController.get(this.templateData.editor)?.hideCodeActions(); CodeActionController.get(this.templateData.editor)?.hideLightBulbWidget(); })); } diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index ac3d98fd4e3..26f94306030 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -8,6 +8,7 @@ import type { IDisposable } from '../../../../../../base/common/lifecycle.js'; import type * as webviewMessages from './webviewMessages.js'; import type { NotebookCellMetadata } from '../../../common/notebookCommon.js'; import type * as rendererApi from 'vscode-notebook-renderer'; +import type { NotebookCellOutputTransferData } from '../../../../../../platform/dnd/browser/dnd.js'; // !! IMPORTANT !! ---------------------------------------------------------------------------------- // import { RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -2894,6 +2895,7 @@ async function webviewPreloads(ctx: PreloadContext) { private hasResizeObserver = false; private renderTaskAbort?: AbortController; + private isImageOutput = false; constructor( private readonly outputId: string, @@ -2914,6 +2916,37 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.addEventListener('mouseleave', () => { postNotebookMessage('mouseleave', { id: outputId }); }); + + // Add drag handler + this.element.addEventListener('dragstart', (e: DragEvent) => { + if (!e.dataTransfer) { + return; + } + + const outputData: NotebookCellOutputTransferData = { + outputId: this.outputId, + }; + + e.dataTransfer.setData('notebook-cell-output', JSON.stringify(outputData)); + }); + + // Add alt key handlers + window.addEventListener('keydown', (e) => { + if (e.altKey) { + this.element.draggable = true; + } + }); + + window.addEventListener('keyup', (e) => { + if (!e.altKey) { + this.element.draggable = this.isImageOutput; + } + }); + + // Handle window blur to reset draggable state + window.addEventListener('blur', () => { + this.element.draggable = this.isImageOutput; + }); } public dispose() { @@ -2933,6 +2966,11 @@ async function webviewPreloads(ctx: PreloadContext) { const errors = preloadErrors.filter((e): e is Error => e instanceof Error); showRenderError(`Error loading preloads`, this.element, errors); } else { + + const imageMimeTypes = ['image/png', 'image/jpeg', 'image/svg']; + this.isImageOutput = imageMimeTypes.includes(content.output.mime); + this.element.draggable = this.isImageOutput; + const item = createOutputItem(this.outputId, content.output.mime, content.metadata, content.output.valueBytes, content.allOutputs, content.output.appended); const controller = new AbortController(); diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index cae7e893d81..6611dd78eaf 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -656,11 +656,11 @@ export abstract class BaseCellViewModel extends Disposable { } updateEditState(newState: CellEditState, source: string) { - this._editStateSource = source; if (newState === this._editState) { return; } + this._editStateSource = source; this._editState = newState; this._onDidChangeState.fire({ editStateChanged: true }); if (this._editState === CellEditState.Preview) { diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 3e08af275e0..dec98508976 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -72,7 +72,7 @@ export type KernelQuickPickContext = { id: string; extension: string } | { notebookEditorId: string } | { id: string; extension: string; notebookEditorId: string } | - { ui?: boolean; notebookEditor?: NotebookEditorWidget }; + { ui?: boolean; notebookEditor?: NotebookEditorWidget; skipIfAlreadySelected?: boolean }; export interface IKernelPickerStrategy { showQuickPick(editor: IActiveNotebookEditor, wantedKernelId?: string): Promise; diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index b4c6ca0c38e..582e967c67c 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -18,7 +18,7 @@ import { selectKernelIcon } from '../notebookIcons.js'; import { KernelPickerMRUStrategy, KernelQuickPickContext } from './notebookKernelQuickPickStrategy.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from '../../common/notebookContextKeys.js'; -import { INotebookKernelHistoryService, INotebookKernelService } from '../../common/notebookKernelService.js'; +import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelService } from '../../common/notebookKernelService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; function getEditorFromContext(editorService: IEditorService, context?: KernelQuickPickContext): INotebookEditor | undefined { @@ -39,6 +39,19 @@ function getEditorFromContext(editorService: IEditorService, context?: KernelQui return editor; } +function shouldSkip( + selected: INotebookKernel | undefined, + controllerId: string | undefined, + extensionId: string | undefined, + context: KernelQuickPickContext | undefined): boolean { + + return !!(selected && ( + (context && 'skipIfAlreadySelected' in context && context.skipIfAlreadySelected) || + // target kernel is already selected + (controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) + )); +} + registerAction2(class extends Action2 { constructor() { super({ @@ -115,11 +128,9 @@ registerAction2(class extends Action2 { const notebook = editor.textModel; const notebookKernelService = accessor.get(INotebookKernelService); - const matchResult = notebookKernelService.getMatchingKernel(notebook); - const { selected } = matchResult; + const { selected } = notebookKernelService.getMatchingKernel(notebook); - if (selected && controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) { - // current kernel is wanted kernel -> done + if (shouldSkip(selected, controllerId, extensionId, context)) { return true; } diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 40c4bd348ab..a47664c7552 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -42,6 +42,7 @@ export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor' export const NOTEBOOK_MULTI_DIFF_EDITOR_ID = 'workbench.editor.notebookMultiTextDiffEditor'; export const INTERACTIVE_WINDOW_EDITOR_ID = 'workbench.editor.interactive'; export const REPL_EDITOR_ID = 'workbench.editor.repl'; +export const NOTEBOOK_OUTPUT_EDITOR_ID = 'workbench.editor.notebookOutputEditor'; export const EXECUTE_REPL_COMMAND_ID = 'replNotebook.input.execute'; @@ -646,7 +647,20 @@ export namespace CellUri { }); } - export function parseCellOutputUri(uri: URI): { notebook: URI; openIn: string; outputId?: string; cellFragment?: string; outputIndex?: number; cellHandle?: number } | undefined { + export function generateOutputEditorUri(notebook: URI, cellId: string, cellIndex: number, outputId: string, outputIndex: number): URI { + return notebook.with({ + scheme: Schemas.vscodeNotebookCellOutput, + query: new URLSearchParams({ + openIn: 'notebookOutputEditor', + notebook: notebook.toString(), + cellIndex: String(cellIndex), + outputId: outputId, + outputIndex: String(outputIndex), + }).toString() + }); + } + + export function parseCellOutputUri(uri: URI): { notebook: URI; openIn: string; outputId?: string; cellFragment?: string; outputIndex?: number; cellHandle?: number; cellIndex?: number } | undefined { return extractCellOutputDetails(uri); } @@ -1002,6 +1016,7 @@ export const NotebookSetting = { stickyScrollMode: 'notebook.stickyScroll.mode', undoRedoPerCell: 'notebook.undoRedoPerCell', consolidatedOutputButton: 'notebook.consolidatedOutputButton', + openOutputInPreviewEditor: 'notebook.output.openInPreviewEditor.enabled', showFoldingControls: 'notebook.showFoldingControls', dragAndDropEnabled: 'notebook.dragAndDropEnabled', cellEditorOptionsCustomizations: 'notebook.editorOptionsCustomizations', diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index 5c218da6ee7..1077d603543 100644 --- a/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -21,7 +21,7 @@ import { CellKind, NotebookSetting } from '../../../common/notebookCommon.js'; import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js'; import { setupInstantiationService, TestNotebookExecutionStateService, withTestNotebook } from '../testNotebookEditor.js'; import { nullExtensionDescription } from '../../../../../services/extensions/common/extensions.js'; -import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../../../chat/common/constants.js'; suite('notebookCellDiagnostics', () => { @@ -73,6 +73,7 @@ suite('notebookCellDiagnostics', () => { name: 'testEditorAgent', isDefault: true, locations: [ChatAgentLocation.Notebook], + modes: [ChatMode.Ask], metadata: {}, slashCommands: [], disambiguation: [], @@ -157,12 +158,12 @@ suite('notebookCellDiagnostics', () => { testExecutionService.fireExecutionChanged(editor.textModel.uri, cell2.handle); await new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); - cell.model.internalMetadata.error = undefined; + const clearMarkers = new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); // on NotebookCellExecution value will make it look like its currently running testExecutionService.fireExecutionChanged(editor.textModel.uri, cell.handle, {} as INotebookCellExecution); - await new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); + await clearMarkers; assert.strictEqual(cell?.executionErrorDiagnostic.get(), undefined); assert.strictEqual(cell2?.executionErrorDiagnostic.get()?.message, 'another bad thing happened', 'cell that was not executed should still have an error'); diff --git a/code/src/vs/workbench/contrib/output/browser/outputServices.ts b/code/src/vs/workbench/contrib/output/browser/outputServices.ts index f644a0f2cc1..bd0506795c6 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/code/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/code/src/vs/workbench/contrib/output/browser/outputView.ts b/code/src/vs/workbench/contrib/output/browser/outputView.ts index a73d4cc25e5..934ea7a8bf2 100644 --- a/code/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/code/src/vs/workbench/contrib/output/browser/outputView.ts @@ -54,7 +54,7 @@ export class OutputViewPane extends FilterViewPane { private readonly editor: OutputEditor; private channelId: string | undefined; - private editorPromise: CancelablePromise | null = null; + private editorPromise: CancelablePromise | null = null; private readonly scrollLockContextKey: IContextKey; get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); } @@ -180,8 +180,7 @@ export class OutputViewPane extends FilterViewPane { const input = this.createInput(channel); if (!this.editor.input || !input.matches(this.editor.input)) { this.editorPromise?.cancel(); - this.editorPromise = createCancelablePromise(token => this.editor.setInput(this.createInput(channel), { preserveFocus: true }, Object.create(null), token) - .then(() => this.editor)); + this.editorPromise = createCancelablePromise(token => this.editor.setInput(this.createInput(channel), { preserveFocus: true }, Object.create(null), token)); } } diff --git a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f5f2925dd5c..705d7dbb192 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -136,10 +136,10 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP this.keybindingFocusContextKey = CONTEXT_KEYBINDING_FOCUS.bindTo(this.contextKeyService); this.searchHistoryDelayer = new Delayer(500); - this.recordKeysAction = new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon)); + this.recordKeysAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon))); this.recordKeysAction.checked = false; - this.sortByPrecedenceAction = new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon)); + this.sortByPrecedenceAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon))); this.sortByPrecedenceAction.checked = false; this.overflowWidgetsDomNode = $('.keybindings-overflow-widgets-container.monaco-editor'); } @@ -355,7 +355,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const fullTextSearchPlaceholder = localize('SearchKeybindings.FullTextSearchPlaceholder', "Type to search in keybindings"); const keybindingsSearchPlaceholder = localize('SearchKeybindings.KeybindingsSearchPlaceholder', "Recording Keys. Press Escape to exit"); - const clearInputAction = new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()); + const clearInputAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults())); const searchContainer = DOM.append(this.headerContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, searchContainer, { diff --git a/code/src/vs/workbench/contrib/preferences/browser/media/preferences.css b/code/src/vs/workbench/contrib/preferences/browser/media/preferences.css deleted file mode 100644 index f1c876cc8f8..00000000000 --- a/code/src/vs/workbench/contrib/preferences/browser/media/preferences.css +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.preferences-editor { - display: flex; - flex-direction: column; -} - -.preferences-editor > .preferences-header { - padding-left: 27px; - padding-right: 32px; - padding-bottom: 11px; - padding-top: 11px; -} - -.preferences-editor .deprecation-warning { - display: flex; - margin-top: 4px; -} - -.preferences-editor .deprecation-warning .icon { - margin-right: 3px; -} - -.preferences-editor .deprecation-warning .learnMore-button { - margin-left: 3px; - text-decoration: underline; -} - -.preferences-editor > .preferences-editors-container.side-by-side-preferences-editor { - flex: 1; -} - -.preferences-editor > .preferences-editors-container.side-by-side-preferences-editor .preferences-header-container { - line-height: 28px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item.disabled { - display: none; -} - -.settings-tabs-widget > .monaco-action-bar .action-item { - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - text-transform: uppercase; - font-size: 11px; - margin-right: 5px; - cursor: pointer; - display: flex; - overflow: hidden; - text-overflow: ellipsis; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.preferences-editor .settings-tabs-widget > .monaco-action-bar .action-item .action-label { - margin-left: 33px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - display: block; - padding: 0px; - border-radius: initial; - background: none !important; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label.folder-settings { - display: flex; -} - -.default-preferences-editor-container > .preferences-header-container > .default-preferences-header, -.settings-tabs-widget > .monaco-action-bar .action-item { - padding: 3px 0px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-title { - text-overflow: ellipsis; - overflow: hidden; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-details { - text-transform: none; - margin-left: 0.5em; - font-size: 10px; - opacity: 0.7; -} - -.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon { - padding-left: 0.3em; - padding-top: 8px; - font-size: 12px; -} - -.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon.hide { - display: none; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label { - color: var(--vscode-panelTitle-inactiveForeground); -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label.checked, -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:hover { - color: var(--vscode-panelTitle-activeForeground); - border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); - outline: 1px solid var(--vscode-contrastActiveBorder, transparent); - outline-offset: -1px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:focus { - border-bottom: 1px solid var(--vscode-focusBorder); - outline: 1px solid transparent; - outline-offset: -1px; -} - -.settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):hover { - outline-style: dashed; -} - -.preferences-header > .settings-header-widget { - flex: 1; - display: flex; - position: relative; - align-self: stretch; -} - -.settings-header-widget > .settings-search-controls > .settings-count-widget { - margin: 6px 0px; - padding: 0px 8px; - border-radius: 2px; - float: left; -} - -.settings-header-widget > .settings-search-controls { - position: absolute; - right: 10px; -} - -.settings-header-widget > .settings-search-controls > .settings-count-widget.hide { - display: none; -} - -.settings-header-widget > .settings-search-container { - flex: 1; -} - -.settings-header-widget > .settings-search-container > .settings-search-input { - vertical-align: middle; -} - -.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { - height: 30px; -} - -.monaco-workbench.vs .settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { - border: 1px solid #ddd; -} - -.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox .input { - font-size: 14px; - padding-left:10px; -} - -.monaco-editor .view-zones > .settings-header-widget { - z-index: 1; -} - -.monaco-editor .settings-header-widget .title-container { - display: flex; - user-select: none; - -webkit-user-select: none; -} - -.monaco-editor .settings-header-widget .title-container .title { - font-weight: bold; - white-space: nowrap; - text-transform: uppercase; -} - -.monaco-editor .settings-header-widget .title-container .message { - white-space: nowrap; -} - -.monaco-editor .settings-group-title-widget { - z-index: 1; -} - -.monaco-editor .settings-group-title-widget .title-container { - width: 100%; - cursor: pointer; - font-weight: bold; - user-select: none; - -webkit-user-select: none; - display: flex; -} - - -.monaco-editor .settings-group-title-widget .title-container .title { - white-space: nowrap; - overflow: hidden; -} - -.monaco-editor.vs-dark .settings-group-title-widget .title-container.focused, -.monaco-editor.vs .settings-group-title-widget .title-container.focused { - outline: none !important; -} - -.monaco-editor .settings-group-title-widget .title-container.focused, -.monaco-editor .settings-group-title-widget .title-container:hover { - background-color: rgba(153, 153, 153, 0.2); -} - -.monaco-editor.hc-black .settings-group-title-widget .title-container.focused { - outline: 1px dotted #f38518; -} - -.monaco-editor.hc-light .settings-group-title-widget .title-container.focused { - outline: 1px dotted #0F4A85; -} - -.monaco-editor .settings-group-title-widget .title-container .codicon { - margin: 0 2px; - width: 16px; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.monaco-editor .dim-configuration { - color: #b1b1b1; -} - -.codicon-settings-edit:hover { - cursor: pointer; -} diff --git a/code/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/code/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index aa1fb1c0536..35647bd1aa8 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/code/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -64,12 +64,28 @@ } .settings-editor > .settings-header > .settings-header-controls { - height: 32px; display: flex; + flex-wrap: wrap; border-bottom: solid 1px; margin-top: 10px; } +.settings-editor > .settings-header > .settings-header-controls .settings-suggestions { + flex: 0 0 100%; + width: 100%; + min-height: 20px; + margin-bottom: 9px; +} + +.settings-editor > .settings-header > .settings-header-controls .settings-suggestions a { + color: var(--vscode-badge-foreground); + background: var(--vscode-badge-background); + cursor: pointer; + margin-right: 4px; + padding: 0px 4px 2px; + border-radius: 4px; +} + .settings-editor > .settings-header > .settings-header-controls .settings-target-container { flex: auto; } @@ -593,11 +609,15 @@ padding-bottom: 26px; } -.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-value-description { +.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-description { display: flex; cursor: pointer; } +.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-description.disabled { + cursor: initial; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-value-checkbox { height: 18px; width: 18px; diff --git a/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 088783e156c..3f7611dd40b 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/code/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -227,3 +227,147 @@ .settings-editor > .settings-body .settings-tree-container .setting-list-widget .setting-list-object-list-row.select-container > select { width: inherit; } + +.settings-tabs-widget > .monaco-action-bar .action-item.disabled { + display: none; +} + +.settings-tabs-widget > .monaco-action-bar .action-item { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + text-transform: uppercase; + font-size: 11px; + margin-right: 5px; + cursor: pointer; + display: flex; + overflow: hidden; + text-overflow: ellipsis; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + display: block; + padding: 0px; + border-radius: initial; + background: none !important; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label.folder-settings { + display: flex; +} + +.settings-tabs-widget > .monaco-action-bar .action-item { + padding: 3px 0px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-title { + text-overflow: ellipsis; + overflow: hidden; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-details { + text-transform: none; + margin-left: 0.5em; + font-size: 10px; + opacity: 0.7; +} + +.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon { + padding-left: 0.3em; + padding-top: 8px; + font-size: 12px; +} + +.settings-tabs-widget .monaco-action-bar .action-item .dropdown-icon.hide { + display: none; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label { + color: var(--vscode-panelTitle-inactiveForeground); +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label.checked, +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:hover { + color: var(--vscode-panelTitle-activeForeground); + border-bottom: 1px solid var(--vscode-panelTitle-activeBorder); + outline: 1px solid var(--vscode-contrastActiveBorder, transparent); + outline-offset: -1px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:focus { + border-bottom: 1px solid var(--vscode-focusBorder); + outline: 1px solid transparent; + outline-offset: -1px; +} + +.settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):hover { + outline-style: dashed; +} + +.settings-header-widget > .settings-search-controls > .settings-count-widget { + margin: 6px 0px; + padding: 0px 8px; + border-radius: 2px; + float: left; +} + +.settings-header-widget > .settings-search-controls { + position: absolute; + right: 10px; +} + +.settings-header-widget > .settings-search-controls > .settings-count-widget.hide { + display: none; +} + +.settings-header-widget > .settings-search-container { + flex: 1; +} + +.settings-header-widget > .settings-search-container > .settings-search-input { + vertical-align: middle; +} + +.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { + height: 30px; +} + +.monaco-workbench.vs .settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox { + border: 1px solid #ddd; +} + +.settings-header-widget > .settings-search-container > .settings-search-input > .monaco-inputbox .input { + font-size: 14px; + padding-left:10px; +} + +.monaco-editor .view-zones > .settings-header-widget { + z-index: 1; +} + +.monaco-editor .settings-header-widget .title-container { + display: flex; + user-select: none; + -webkit-user-select: none; +} + +.monaco-editor .settings-header-widget .title-container .title { + font-weight: bold; + white-space: nowrap; + text-transform: uppercase; +} + +.monaco-editor .settings-header-widget .title-container .message { + white-space: nowrap; +} + +.monaco-editor .dim-configuration { + color: #b1b1b1; +} + +.codicon-settings-edit:hover { + cursor: pointer; +} diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index d69acc0a677..f1a4ac3ff04 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -8,7 +8,6 @@ import { Disposable, DisposableStore, MutableDisposable } from '../../../../base import { Schemas } from '../../../../base/common/network.js'; import { isBoolean, isObject, isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import './media/preferences.css'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { Context as SuggestContext } from '../../../../editor/contrib/suggest/browser/suggest.js'; import * as nls from '../../../../nls.js'; @@ -31,7 +30,6 @@ import { ResourceContextKey, RemoteNameContext, WorkbenchStateContext } from '.. import { ExplorerFolderContext, ExplorerRootContext } from '../../files/common/files.js'; import { KeybindingsEditor } from './keybindingsEditor.js'; import { ConfigureLanguageBasedSettingsAction } from './preferencesActions.js'; -import { SettingsEditorContribution } from './preferencesEditor.js'; import { preferencesOpenSettingsIcon } from './preferencesIcons.js'; import { SettingsEditor2, SettingsFocusContext } from './settingsEditor2.js'; import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ACCEPT_WHEN, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REJECT_WHEN, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from '../common/preferences.js'; @@ -44,11 +42,14 @@ import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContributio import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; -import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { resolveCommandsContext } from '../../../browser/parts/editor/editorCommandsContext.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IListService } from '../../../../platform/list/browser/listService.js'; +import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; +import { IPreferencesRenderer, WorkspaceSettingsRenderer, UserSettingsRenderer } from './preferencesRenderers.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -1258,6 +1259,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), ContextKeyExpr.not('isInDiffEditor')); + registerOpenUserSettingsEditorFromJsonActionDisposables.clear(); registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ @@ -1313,6 +1315,51 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo } } +class SettingsEditorContribution extends Disposable { + static readonly ID: string = 'editor.contrib.settings'; + + private currentRenderer: IPreferencesRenderer | undefined; + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + ) { + super(); + this._createPreferencesRenderer(); + this._register(this.editor.onDidChangeModel(e => this._createPreferencesRenderer())); + this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this._createPreferencesRenderer())); + } + + private async _createPreferencesRenderer(): Promise { + this.disposables.clear(); + this.currentRenderer = undefined; + + const model = this.editor.getModel(); + if (model && /\.(json|code-workspace)$/.test(model.uri.path)) { + // Fast check: the preferences renderer can only appear + // in settings files or workspace files + const settingsModel = await this.preferencesService.createPreferencesEditorModel(model.uri); + if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { + this.disposables.add(settingsModel); + switch (settingsModel.configurationTarget) { + case ConfigurationTarget.WORKSPACE: + this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel)); + break; + default: + this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel)); + break; + } + } + + this.currentRenderer?.render(); + } + } +} + + function getEditorGroupFromArguments(accessor: ServicesAccessor, args: unknown[]): IEditorGroup | undefined { const context = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService)); return context.groupedEditors[0]?.group; diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts index 46fa025a87e..a9b88a30f57 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesActions.ts @@ -19,7 +19,6 @@ import { MenuId, MenuRegistry, isIMenuItem } from '../../../../platform/actions/ import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { isLocalizedString } from '../../../../platform/action/common/action.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingsRegistry } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; export class ConfigureLanguageBasedSettingsAction extends Action { @@ -120,26 +119,6 @@ CommandsRegistry.registerCommand('_getAllCommands', function (accessor, filterBy }); } } - for (const command of KeybindingsRegistry.getDefaultKeybindings()) { - if (filterByPrecondition && !contextKeyService.contextMatchesRules(command.when ?? undefined)) { - continue; - } - - const keybinding = keybindingService.lookupKeybinding(command.command ?? ''); - if (!keybinding) { - continue; - } - - if (actions.some(a => a.command === command.command)) { - continue; - } - actions.push({ - command: command.command ?? '', - label: command.command ?? '', - keybinding: keybinding?.getLabel() ?? 'Not set', - precondition: command.when?.serialize() - }); - } return actions; }); diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts deleted file mode 100644 index aed3a321c96..00000000000 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from './preferencesRenderers.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; -import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; - -export class SettingsEditorContribution extends Disposable { - static readonly ID: string = 'editor.contrib.settings'; - - private currentRenderer: IPreferencesRenderer | undefined; - private readonly disposables = this._register(new DisposableStore()); - - constructor( - private readonly editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPreferencesService private readonly preferencesService: IPreferencesService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService - ) { - super(); - this._createPreferencesRenderer(); - this._register(this.editor.onDidChangeModel(e => this._createPreferencesRenderer())); - this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this._createPreferencesRenderer())); - } - - private async _createPreferencesRenderer(): Promise { - this.disposables.clear(); - this.currentRenderer = undefined; - - const model = this.editor.getModel(); - if (model && /\.(json|code-workspace)$/.test(model.uri.path)) { - // Fast check: the preferences renderer can only appear - // in settings files or workspace files - const settingsModel = await this.preferencesService.createPreferencesEditorModel(model.uri); - if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { - this.disposables.add(settingsModel); - switch (settingsModel.configurationTarget) { - case ConfigurationTarget.WORKSPACE: - this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel)); - break; - default: - this.currentRenderer = this.disposables.add(this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel)); - break; - } - } - - this.currentRenderer?.render(); - } - } -} diff --git a/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 762a4622a6b..6ed14c391b6 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -17,10 +17,10 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IAiRelatedInformationService, RelatedInformationType, SettingInformationResult } from '../../../services/aiRelatedInformation/common/aiRelatedInformation.js'; import { TfIdfCalculator, TfIdfDocument } from '../../../../base/common/tfIdf.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { nullRange } from '../../../services/preferences/common/preferencesModels.js'; +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export interface IEndpointDetails { urlBase?: string; @@ -170,7 +170,7 @@ export class SettingMatches { } private _toAlphaNumeric(s: string): string { - return s.replace(/[^A-Za-z0-9]+/g, ''); + return s.replace(/[^\p{L}\p{N}]+/gu, ''); } private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] { @@ -350,14 +350,11 @@ export class SettingMatches { } } -class AiRelatedInformationSearchKeysProvider { - private settingKeys: string[] = []; +class AiSettingsSearchKeysProvider { private settingsRecord: IStringDictionary = {}; private currentPreferencesModel: ISettingsEditorModel | undefined; - constructor( - private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { } + constructor() { } updateModel(preferencesModel: ISettingsEditorModel) { if (preferencesModel === this.currentPreferencesModel) { @@ -369,13 +366,9 @@ class AiRelatedInformationSearchKeysProvider { } private refresh() { - this.settingKeys = []; this.settingsRecord = {}; - if ( - !this.currentPreferencesModel || - !this.aiRelatedInformationService.isEnabled() - ) { + if (!this.currentPreferencesModel) { return; } @@ -385,32 +378,25 @@ class AiRelatedInformationSearchKeysProvider { } for (const section of group.sections) { for (const setting of section.settings) { - this.settingKeys.push(setting.key); this.settingsRecord[setting.key] = setting; } } } } - getSettingKeys(): string[] { - return this.settingKeys; - } - getSettingsRecord(): IStringDictionary { return this.settingsRecord; } } -class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { - private static readonly AI_RELATED_INFORMATION_MAX_PICKS = 5; +class AiSettingsSearchProvider implements IRemoteSearchProvider { + private static readonly AI_SETTINGS_SEARCH_MAX_PICKS = 5; - private readonly _keysProvider: AiRelatedInformationSearchKeysProvider; + private readonly _keysProvider: AiSettingsSearchKeysProvider; private _filter: string = ''; - constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { - this._keysProvider = new AiRelatedInformationSearchKeysProvider(aiRelatedInformationService); + constructor(private readonly aiSettingsSearchService: IAiSettingsSearchService) { + this._keysProvider = new AiSettingsSearchKeysProvider(); } setFilter(filter: string) { @@ -420,41 +406,38 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if ( !this._filter || - !this.aiRelatedInformationService.isEnabled() + !this.aiSettingsSearchService.isEnabled() ) { return null; } this._keysProvider.updateModel(preferencesModel); + this.aiSettingsSearchService.startSearch(this._filter, token); return { - filterMatches: await this.getAiRelatedInformationItems(token), + filterMatches: await this.getAiSettingsSearchItems(token), exactMatch: false }; } - private async getAiRelatedInformationItems(token: CancellationToken) { + private async getAiSettingsSearchItems(token: CancellationToken): Promise { const settingsRecord = this._keysProvider.getSettingsRecord(); - const filterMatches: ISettingMatch[] = []; - const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( - this._filter, - [RelatedInformationType.SettingInformation], - token - ) as SettingInformationResult[]; - relatedInformation.sort((a, b) => b.weight - a.weight); - - for (const info of relatedInformation) { - if (filterMatches.length === AiRelatedInformationSearchProvider.AI_RELATED_INFORMATION_MAX_PICKS) { + const settings = await this.aiSettingsSearchService.getEmbeddingsResults(this._filter, token); + if (!settings) { + return []; + } + + for (const settingKey of settings) { + if (filterMatches.length === AiSettingsSearchProvider.AI_SETTINGS_SEARCH_MAX_PICKS) { break; } - const pick = info.setting; filterMatches.push({ - setting: settingsRecord[pick], - matches: [settingsRecord[pick].range], + setting: settingsRecord[settingKey], + matches: [settingsRecord[settingKey].range], matchType: SettingMatchType.RemoteMatch, keyMatchScore: 0, - score: info.weight + score: 0 // the results are sorted upstream. }); } @@ -560,29 +543,21 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { } class RemoteSearchProvider implements IRemoteSearchProvider { - private adaSearchProvider: AiRelatedInformationSearchProvider | undefined; - private tfIdfSearchProvider: TfIdfSearchProvider | undefined; + private aiSettingsSearchProvider: AiSettingsSearchProvider; + private tfIdfSearchProvider: TfIdfSearchProvider; private filter: string = ''; constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { - } - - private initializeSearchProviders() { - if (this.aiRelatedInformationService.isEnabled()) { - this.adaSearchProvider ??= new AiRelatedInformationSearchProvider(this.aiRelatedInformationService); - } - this.tfIdfSearchProvider ??= new TfIdfSearchProvider(); + this.aiSettingsSearchProvider = new AiSettingsSearchProvider(this.aiSettingsSearchService); + this.tfIdfSearchProvider = new TfIdfSearchProvider(); } setFilter(filter: string): void { - this.initializeSearchProviders(); this.filter = filter; - if (this.adaSearchProvider) { - this.adaSearchProvider.setFilter(filter); - } - this.tfIdfSearchProvider!.setFilter(filter); + this.tfIdfSearchProvider.setFilter(filter); + this.aiSettingsSearchProvider.setFilter(filter); } async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { @@ -590,17 +565,16 @@ class RemoteSearchProvider implements IRemoteSearchProvider { return null; } - if (!this.adaSearchProvider) { - return this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + if (!this.aiSettingsSearchService.isEnabled()) { + return this.tfIdfSearchProvider.searchModel(preferencesModel, token); } - // Use TF-IDF search as a fallback, ref https://github.com/microsoft/vscode/issues/224946 - let results = await this.adaSearchProvider.searchModel(preferencesModel, token); + let results = await this.aiSettingsSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } if (!token.isCancellationRequested) { - results = await this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + results = await this.tfIdfSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index e5de67fb278..485bdeb4b0f 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -41,7 +41,7 @@ import { ITOCEntry, getCommonlyUsedData, tocData } from './settingsLayout.js'; import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from './settingsTree.js'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from './settingsTreeModels.js'; import { createTOCIterator, TOCTree, TOCTreeModel } from './tocTree.js'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG, getExperimentalExtensionToggleData } from '../common/preferences.js'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG, getExperimentalExtensionToggleData, wordifyKey } from '../common/preferences.js'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; @@ -68,7 +68,7 @@ import { IEditorProgressService } from '../../../../platform/progress/common/pro import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { CodeWindow } from '../../../../base/browser/window.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; - +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export const enum SettingsFocusContext { Search, @@ -168,6 +168,7 @@ export class SettingsEditor2 extends EditorPane { private countElement!: HTMLElement; private controlsElement!: HTMLElement; private settingsTargetsWidget!: SettingsTargetsWidget; + private suggestionsDiv!: HTMLElement; private splitView!: SplitView; @@ -226,6 +227,8 @@ export class SettingsEditor2 extends EditorPane { private readonly inputChangeListener: MutableDisposable; + private readonly searchSuggestionDisposables: DisposableStore = this._register(new DisposableStore()); + constructor( group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @@ -249,6 +252,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.searchDelayer = new Delayer(300); @@ -668,6 +672,11 @@ export class SettingsEditor2 extends EditorPane { const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); headerControlsContainer.style.borderColor = asCssVariable(settingsHeaderBorder); + this.suggestionsDiv = DOM.append(headerControlsContainer, $('div.settings-suggestions')); + if (this.configurationService.getValue('workbench.settings.showSuggestions') !== true) { + this.suggestionsDiv.hidden = true; + } + const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true })); this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL; @@ -1606,6 +1615,7 @@ export class SettingsEditor2 extends EditorPane { } private async triggerSearch(query: string): Promise { + this.clearSearchSuggestions(); const progressRunner = this.editorProgressService.show(true, 800); this.viewState.tagFilters = new Set(); this.viewState.extensionFilters = new Set(); @@ -1700,13 +1710,39 @@ export class SettingsEditor2 extends EditorPane { return; } const localResults = await this.localFilterPreferences(query, searchInProgress.token); - if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { - await this.remoteSearchPreferences(query, searchInProgress.token); + let remoteResults = null; + if ((!localResults || !localResults.exactMatch) && !searchInProgress.token.isCancellationRequested) { + remoteResults = await this.remoteSearchPreferences(query, searchInProgress.token); + } + + if (searchInProgress.token.isCancellationRequested) { + return; } // Update UI only after all the search results are in // ref https://github.com/microsoft/vscode/issues/224946 this.onDidFinishSearch(); + + if (remoteResults?.filterMatches.length) { + if (this.aiSettingsSearchService.isEnabled() && !searchInProgress.token.isCancellationRequested) { + const rankedResults = await this.aiSettingsSearchService.getLLMRankedResults(query, searchInProgress.token); + if (!searchInProgress.token.isCancellationRequested) { + if (rankedResults === null) { + this.logService.trace('No ranked results found'); + } else { + this.logService.trace(`Got ranked results ${rankedResults.join(', ')}`); + // Make a suggestion if the setting isn't in the top five results. + const firstFewResults = new Set([ + ...(localResults?.filterMatches.map(m => m.setting.key) ?? []), + ...(remoteResults.filterMatches.map(m => m.setting.key)) + ].slice(0, 5)); + const suggestedResults = rankedResults.filter(r => !firstFewResults.has(r)); + this.logService.trace(`Filtering ranked results down to ${suggestedResults.join(', ')}`); + this.setSearchSuggestions(suggestedResults); + } + } + } + } }); } @@ -1721,6 +1757,43 @@ export class SettingsEditor2 extends EditorPane { this.renderTree(undefined, true); } + private clearSearchSuggestions(): void { + this.searchSuggestionDisposables.clear(); + this.suggestionsDiv.innerText = ''; + } + + private setSearchSuggestions(suggestions: string[]): void { + this.clearSearchSuggestions(); + + if (suggestions.length === 0) { + return; + } + + this.suggestionsDiv.innerText = localize('suggestionsPrefix', "Did you mean: "); + suggestions.forEach((suggestion, idx) => { + const suggestionLink = document.createElement('a'); + suggestionLink.textContent = wordifyKey(suggestion); + suggestionLink.tabIndex = 0; + suggestionLink.setAttribute('aria-label', suggestion); + this.searchSuggestionDisposables.add(DOM.addDisposableListener(suggestionLink, 'click', (e) => { + e.preventDefault(); + this.searchWidget.setValue(suggestion); + this.focusSearch(); + })); + this.searchSuggestionDisposables.add(DOM.addDisposableListener(suggestionLink, 'keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.searchWidget.setValue(suggestion); + this.focusSearch(); + } + })); + this.suggestionsDiv.appendChild(suggestionLink); + if (idx < suggestions.length - 1) { + this.suggestionsDiv.appendChild(document.createTextNode(', ')); + } + }); + } + private localFilterPreferences(query: string, token: CancellationToken): Promise { const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, token); diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 43df1e38b5f..0733ae0ad50 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -302,25 +302,3 @@ export const tocData: ITOCEntry = { } ] }; - -export const knownAcronyms = new Set(); -[ - 'css', - 'html', - 'scss', - 'less', - 'json', - 'js', - 'ts', - 'ie', - 'id', - 'php', - 'scm', -].forEach(str => knownAcronyms.add(str)); - -export const knownTermMappings = new Map(); -knownTermMappings.set('power shell', 'PowerShell'); -knownTermMappings.set('powershell', 'PowerShell'); -knownTermMappings.set('javascript', 'JavaScript'); -knownTermMappings.set('typescript', 'TypeScript'); -knownTermMappings.set('github', 'GitHub'); diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 2f49546c8e4..caa55406a72 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1989,7 +1989,7 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label')); const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer)); - const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); + const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement)); const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); @@ -2008,20 +2008,6 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender template.onChange!(checkbox.checked); })); - // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety - // Also have to ignore embedded links - too buried to stop propagation - toDispose.add(DOM.addDisposableListener(descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { - const targetElement = e.target; - - // Toggle target checkbox - if (targetElement.tagName.toLowerCase() !== 'a') { - template.checkbox.checked = !template.checkbox.checked; - template.onChange!(checkbox.checked); - } - DOM.EventHelper.stop(e); - })); - - checkbox.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); const toolbar = this.renderSettingToolbar(toolbarContainer); @@ -2059,6 +2045,26 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { template.onChange = undefined; template.checkbox.checked = dataElement.value; + if (dataElement.hasPolicyValue) { + template.checkbox.disable(); + template.descriptionElement.classList.add('disabled'); + } else { + template.checkbox.enable(); + template.descriptionElement.classList.remove('disabled'); + + // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety + // Also have to ignore embedded links - too buried to stop propagation + template.elementDisposables.add(DOM.addDisposableListener(template.descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { + const targetElement = e.target; + + // Toggle target checkbox + if (targetElement.tagName.toLowerCase() !== 'a') { + template.checkbox.checked = !template.checkbox.checked; + template.onChange!(template.checkbox.checked); + } + DOM.EventHelper.stop(e); + })); + } template.checkbox.setTitle(dataElement.setting.key); template.onChange = onChange; } diff --git a/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 629e04c3744..2ae55f17398 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -9,8 +9,8 @@ import { isUndefinedOrNull } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ConfigurationTarget, IConfigurationValue } from '../../../../platform/configuration/common/configuration.js'; import { SettingsTarget } from './preferencesWidgets.js'; -import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from './settingsLayout.js'; -import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers } from '../common/preferences.js'; +import { ITOCEntry, tocData } from './settingsLayout.js'; +import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers, wordifyKey } from '../common/preferences.js'; import { IExtensionSetting, ISearchResult, ISetting, ISettingMatch, SettingMatchType, SettingValueType } from '../../../services/preferences/common/preferences.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from '../../../services/configuration/common/configuration.js'; @@ -727,24 +727,6 @@ export function settingKeyToDisplayFormat(key: string, groupId: string = '', isL return { category, label }; } -function wordifyKey(key: string): string { - key = key - .replace(/\.([a-z0-9])/g, (_, p1) => ` \u203A ${p1.toUpperCase()}`) // Replace dot with spaced '>' - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar - .replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo - .replace(/\b\w+\b/g, match => { // Upper casing known acronyms - return knownAcronyms.has(match.toLowerCase()) ? - match.toUpperCase() : - match; - }); - - for (const [k, v] of knownTermMappings) { - key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v); - } - - return key; -} - /** * Removes redundant sections of the category label. * A redundant section is a section already reflected in the groupId. diff --git a/code/src/vs/workbench/contrib/preferences/common/preferences.ts b/code/src/vs/workbench/contrib/preferences/common/preferences.ts index c2d3fe0ddae..fb9655c44b8 100644 --- a/code/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/code/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -178,3 +178,43 @@ export function compareTwoNullableNumbers(a: number | undefined, b: number | und export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "Preview setting: this setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "Experimental setting: this setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); + +export const knownAcronyms = new Set(); +[ + 'css', + 'html', + 'scss', + 'less', + 'json', + 'js', + 'ts', + 'ie', + 'id', + 'php', + 'scm', +].forEach(str => knownAcronyms.add(str)); + +export const knownTermMappings = new Map(); +knownTermMappings.set('power shell', 'PowerShell'); +knownTermMappings.set('powershell', 'PowerShell'); +knownTermMappings.set('javascript', 'JavaScript'); +knownTermMappings.set('typescript', 'TypeScript'); +knownTermMappings.set('github', 'GitHub'); + +export function wordifyKey(key: string): string { + key = key + .replace(/\.([a-z0-9])/g, (_, p1) => ` \u203A ${p1.toUpperCase()}`) // Replace dot with spaced '>' + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar + .replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo + .replace(/\b\w+\b/g, match => { // Upper casing known acronyms + return knownAcronyms.has(match.toLowerCase()) ? + match.toUpperCase() : + match; + }); + + for (const [k, v] of knownTermMappings) { + key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v); + } + + return key; +} diff --git a/code/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/code/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 3ae6694f964..2de14ff31ee 100644 --- a/code/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/code/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/code/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/code/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 3e8b69d16af..8026fbf387d 100644 --- a/code/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/code/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -30,11 +30,11 @@ interface IConfiguration extends IWindowsConfiguration { editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; security?: { workspace?: { trust?: { enabled?: boolean } }; restrictUNCAccess?: boolean }; window: IWindowSettings; - workbench?: { enableExperiments?: boolean }; + workbench?: { enableExperiments?: boolean; settings?: { showSuggestions?: boolean } }; telemetry?: { feedback?: { enabled?: boolean } }; _extensionsGallery?: { enablePPE?: boolean }; accessibility?: { verbosity?: { debug?: boolean } }; - chat?: { unifiedChatView?: boolean; useFileStorage?: boolean }; + chat?: { useFileStorage?: boolean }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -49,10 +49,10 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'editor.accessibilitySupport', 'security.workspace.trust.enabled', 'workbench.enableExperiments', + 'workbench.settings.showSuggestions', '_extensionsGallery.enablePPE', 'security.restrictUNCAccess', 'accessibility.verbosity.debug', - ChatConfiguration.UnifiedChatView, ChatConfiguration.UseFileStorage, 'telemetry.feedback.enabled' ]; @@ -69,9 +69,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); - private readonly unifiedChatView = new ChangeObserver('boolean'); private readonly useFileStorage = new ChangeObserver('boolean'); private readonly telemetryFeedbackEnabled = new ChangeObserver('boolean'); + private readonly showSuggestions = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -151,7 +151,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Debug accessibility verbosity processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); - processChanged(this.unifiedChatView.handleChange(config.chat?.unifiedChatView)); processChanged(this.useFileStorage.handleChange(config.chat?.useFileStorage)); } @@ -164,6 +163,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Enable Feedback processChanged(this.telemetryFeedbackEnabled.handleChange(config.telemetry?.feedback?.enabled)); + // Settings editor suggestions + processChanged(this.showSuggestions.handleChange(config.workbench?.settings?.showSuggestions)); + if (askToRelaunch && changed && this.hostService.hasFocus) { this.doConfirm( isNative ? diff --git a/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index b716bcd1d0c..5de04851541 100644 --- a/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/code/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -20,7 +20,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js'; @@ -188,6 +188,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo } this.notificationService.notify({ severity: Severity.Info, + priority: NotificationPriority.OPTIONAL, message: localize( { diff --git a/code/src/vs/workbench/contrib/scm/browser/activity.ts b/code/src/vs/workbench/contrib/scm/browser/activity.ts index e2abe63d3b6..bd29ee8c06d 100644 --- a/code/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/code/src/vs/workbench/contrib/scm/browser/activity.ts @@ -177,7 +177,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe ); } - // Ssource control provider status bar entry + // Source control provider status bar entry if (this.scmService.repositoryCount > 1) { const repositoryStatusbarEntry: IStatusbarEntry = { name: localize('status.scm.provider', "Source Control Provider"), diff --git a/code/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css b/code/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css index 5a9e4a2536e..d1490f77723 100644 --- a/code/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css +++ b/code/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css @@ -25,55 +25,91 @@ display: none; } -.monaco-editor .dirty-diff-added { - border-left-color: var(--vscode-editorGutter-addedBackground); +.monaco-editor .dirty-diff-added:not(.pattern) { border-left-style: solid; } -.monaco-editor .dirty-diff-added:before { +.monaco-editor .dirty-diff-added.primary { + border-left-color: var(--vscode-editorGutter-addedBackground); +} + +.monaco-editor .dirty-diff-added.primary:before { background: var(--vscode-editorGutter-addedBackground); } -.monaco-editor .dirty-diff-added-pattern { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-added.secondary { + border-left-color: var(--vscode-editorGutter-addedSecondaryBackground); +} + +.monaco-editor .dirty-diff-added.secondary:before { + background: var(--vscode-editorGutter-addedSecondaryBackground); +} + +.monaco-editor .dirty-diff-added.pattern { background-repeat: repeat-y; } -.monaco-editor .dirty-diff-added-pattern:before { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-added.pattern:before { transform: translateX(3px); } -.monaco-editor .dirty-diff-modified { - border-left-color: var(--vscode-editorGutter-modifiedBackground); +.monaco-editor .dirty-diff-added.pattern.primary, +.monaco-editor .dirty-diff-added.pattern.primary:before { + background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-editor .dirty-diff-added.pattern.secondary, +.monaco-editor .dirty-diff-added.pattern.secondary:before { + background-image: linear-gradient(45deg, var(--vscode-editorGutter-addedSecondaryBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedSecondaryBackground) 50%, var(--vscode-editorGutter-addedSecondaryBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-editor .dirty-diff-modified:not(.pattern) { border-left-style: solid; } -.monaco-editor .dirty-diff-modified:before { +.monaco-editor .dirty-diff-modified.primary { + border-left-color: var(--vscode-editorGutter-modifiedBackground); +} + +.monaco-editor .dirty-diff-modified.primary:before { background: var(--vscode-editorGutter-modifiedBackground); } -.monaco-editor .dirty-diff-modified-pattern { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-modifiedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedBackground) 50%, var(--vscode-editorGutter-modifiedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-modified.secondary { + border-left-color: var(--vscode-editorGutter-modifiedSecondaryBackground); +} + +.monaco-editor .dirty-diff-modified.secondary:before { + background: var(--vscode-editorGutter-modifiedSecondaryBackground); +} + +.monaco-editor .dirty-diff-modified.pattern { background-repeat: repeat-y; } -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added-pattern, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified-pattern { - transition: opacity 0.5s; +.monaco-editor .dirty-diff-modified.pattern:before { + transform: translateX(3px); } -.monaco-editor .dirty-diff-modified-pattern:before { +.monaco-editor .dirty-diff-modified.pattern.primary, +.monaco-editor .dirty-diff-modified.pattern.primary:before { background-image: linear-gradient(-45deg, var(--vscode-editorGutter-modifiedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedBackground) 50%, var(--vscode-editorGutter-modifiedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); - transform: translateX(3px); +} + +.monaco-editor .dirty-diff-modified.pattern.secondary, +.monaco-editor .dirty-diff-modified.pattern.secondary:before { + background-image: linear-gradient(45deg, var(--vscode-editorGutter-modifiedSecondaryBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedSecondaryBackground) 50%, var(--vscode-editorGutter-modifiedSecondaryBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added, +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified, +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted { + transition: opacity 0.5s; } .monaco-editor .margin:hover .dirty-diff-added, -.monaco-editor .margin:hover .dirty-diff-added-pattern, .monaco-editor .margin:hover .dirty-diff-modified, -.monaco-editor .margin:hover .dirty-diff-modified-pattern { +.monaco-editor .margin:hover .dirty-diff-deleted { opacity: 1; } @@ -87,10 +123,17 @@ z-index: 9; border-top: 4px solid transparent; border-bottom: 4px solid transparent; - border-left: 4px solid var(--vscode-editorGutter-deletedBackground); pointer-events: none; } +.monaco-editor .dirty-diff-deleted.primary:after { + border-left: 4px solid var(--vscode-editorGutter-deletedBackground); +} + +.monaco-editor .dirty-diff-deleted.secondary:after { + border-left: 4px solid var(--vscode-editorGutter-deletedSecondaryBackground); +} + .monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted:after { transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear, opacity 0.5s; } @@ -102,6 +145,14 @@ bottom: 0; } +.monaco-editor .dirty-diff-deleted.primary:before { + background: var(--vscode-editorGutter-deletedBackground); +} + +.monaco-editor .dirty-diff-deleted.secondary:before { + background: var(--vscode-editorGutter-deletedSecondaryBackground); +} + .monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted:before { transition: height 80ms linear; } diff --git a/code/src/vs/workbench/contrib/scm/browser/media/scm.css b/code/src/vs/workbench/contrib/scm/browser/media/scm.css index 6faa64d0e35..a38b3742801 100644 --- a/code/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/code/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -650,7 +650,3 @@ border-radius: 2px; opacity: 0.5; } - -.monaco-workbench .part.statusbar > .items-container > .statusbar-item .codicon.codicon-repo { - padding-top: 1px; -} diff --git a/code/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/code/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index ac52c163d58..390c8b44ff3 100644 --- a/code/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/code/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -13,17 +13,19 @@ import { ModelDecorationOptions } from '../../../../editor/common/model/textMode import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition } from '../../../../editor/common/model.js'; +import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition, IModelDeltaDecoration } from '../../../../editor/common/model.js'; import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ChangeType, getChangeType, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; +import { ChangeType, getChangeType, IQuickDiffService, QuickDiffProvider, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; import { QuickDiffModel, IQuickDiffModelService } from './quickDiffModel.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyTrueExpr, ContextKeyFalseExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { autorun, autorunWithStore, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; +import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; export const quickDiffDecorationCount = new RawContextKey('quickDiffDecorationCount', 0); @@ -58,16 +60,22 @@ class QuickDiffDecorator extends Disposable { } private addedOptions: ModelDecorationOptions; + private addedSecondaryOptions: ModelDecorationOptions; private addedPatternOptions: ModelDecorationOptions; + private addedSecondaryPatternOptions: ModelDecorationOptions; private modifiedOptions: ModelDecorationOptions; + private modifiedSecondaryOptions: ModelDecorationOptions; private modifiedPatternOptions: ModelDecorationOptions; + private modifiedSecondaryPatternOptions: ModelDecorationOptions; private deletedOptions: ModelDecorationOptions; + private deletedSecondaryOptions: ModelDecorationOptions; private decorationsCollection: IEditorDecorationsCollection | undefined; constructor( private readonly codeEditor: ICodeEditor, private readonly quickDiffModelRef: IReference, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService ) { super(); @@ -77,37 +85,38 @@ class QuickDiffDecorator extends Disposable { const minimap = decorations === 'all' || decorations === 'minimap'; const diffAdded = nls.localize('diffAdded', 'Added lines'); - this.addedOptions = QuickDiffDecorator.createDecoration('dirty-diff-added', diffAdded, { - gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, - isWholeLine: true - }); - this.addedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, { + const diffAddedOptions = { gutter, overview: { active: overview, color: overviewRulerAddedForeground }, minimap: { active: minimap, color: minimapGutterAddedBackground }, isWholeLine: true - }); + }; + this.addedOptions = QuickDiffDecorator.createDecoration('dirty-diff-added primary', diffAdded, diffAddedOptions); + this.addedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added primary pattern', diffAdded, diffAddedOptions); + this.addedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-added secondary', diffAdded, diffAddedOptions); + this.addedSecondaryPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added secondary pattern', diffAdded, diffAddedOptions); + const diffModified = nls.localize('diffModified', 'Changed lines'); - this.modifiedOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified', diffModified, { + const diffModifiedOptions = { gutter, overview: { active: overview, color: overviewRulerModifiedForeground }, minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true - }); - this.modifiedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, { - gutter, - overview: { active: overview, color: overviewRulerModifiedForeground }, - minimap: { active: minimap, color: minimapGutterModifiedBackground }, - isWholeLine: true - }); - this.deletedOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), { + }; + this.modifiedOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified primary', diffModified, diffModifiedOptions); + this.modifiedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified primary pattern', diffModified, diffModifiedOptions); + this.modifiedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified secondary', diffModified, diffModifiedOptions); + this.modifiedSecondaryPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified secondary pattern', diffModified, diffModifiedOptions); + + const diffDeleted = nls.localize('diffDeleted', 'Removed lines'); + const diffDeletedOptions = { gutter, overview: { active: overview, color: overviewRulerDeletedForeground }, minimap: { active: minimap, color: minimapGutterDeletedBackground }, isWholeLine: false - }); + }; + this.deletedOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted primary', diffDeleted, diffDeletedOptions); + this.deletedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted secondary', diffDeleted, diffDeletedOptions); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { @@ -123,44 +132,66 @@ class QuickDiffDecorator extends Disposable { return; } - const visibleQuickDiffs = this.quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); - const decorations = this.quickDiffModelRef.object.changes - .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) - .map((labeledChange) => { - const change = labeledChange.change; - const changeType = getChangeType(change); - const startLineNumber = change.modifiedStartLineNumber; - const endLineNumber = change.modifiedEndLineNumber || startLineNumber; - - switch (changeType) { - case ChangeType.Add: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.added ? this.addedPatternOptions : this.addedOptions - }; - case ChangeType.Delete: - return { - range: { - startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, - endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE - }, - options: this.deletedOptions - }; - case ChangeType.Modify: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions - }; - } - }); + const primaryQuickDiff = this.quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const primaryQuickDiffChanges = this.quickDiffModelRef.object.changes.filter(change => change.providerId === primaryQuickDiff?.id); + + const decorations: IModelDeltaDecoration[] = []; + for (const change of this.quickDiffModelRef.object.changes) { + const quickDiff = this.quickDiffModelRef.object.quickDiffs + .find(quickDiff => quickDiff.id === change.providerId); + + // Skip quick diffs that are not visible + if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { + continue; + } + + if (quickDiff.kind !== 'primary' && primaryQuickDiffChanges.some(c => c.change2.modified.overlapOrTouch(change.change2.modified))) { + // Overlap with primary quick diff changes + continue; + } + + const changeType = getChangeType(change.change); + const startLineNumber = change.change.modifiedStartLineNumber; + const endLineNumber = change.change.modifiedEndLineNumber || startLineNumber; + + switch (changeType) { + case ChangeType.Add: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? pattern.added ? this.addedPatternOptions : this.addedOptions + : pattern.added ? this.addedSecondaryPatternOptions : this.addedSecondaryOptions + }); + break; + case ChangeType.Delete: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, + endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? this.deletedOptions + : this.deletedSecondaryOptions + }); + break; + case ChangeType.Modify: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions + : pattern.modified ? this.modifiedSecondaryPatternOptions : this.modifiedSecondaryOptions + }); + break; + } + } if (!this.decorationsCollection) { this.decorationsCollection = this.codeEditor.createDecorationsCollection(decorations); @@ -190,6 +221,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben private readonly quickDiffDecorationCount: IContextKey; private readonly activeEditor: IObservable; + private readonly quickDiffProviders: IObservable; // Resource URI -> Code Editor Id -> Decoration (Disposable) private readonly decorators = new ResourceMap>(); @@ -201,6 +233,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService, ) { @@ -212,6 +245,9 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.activeEditor = observableFromEvent(this, this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + this.quickDiffProviders = observableFromEvent(this, + this.quickDiffService.onDidChangeQuickDiffProviders, () => this.quickDiffService.providers); + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); this.onDidChangeConfiguration(); @@ -257,16 +293,14 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben .monaco-editor .dirty-diff-modified { border-left-width:${state.width}px; } - .monaco-editor .dirty-diff-added-pattern, - .monaco-editor .dirty-diff-added-pattern:before, - .monaco-editor .dirty-diff-modified-pattern, - .monaco-editor .dirty-diff-modified-pattern:before { + .monaco-editor .dirty-diff-added.pattern, + .monaco-editor .dirty-diff-added.pattern:before, + .monaco-editor .dirty-diff-modified.pattern, + .monaco-editor .dirty-diff-modified.pattern:before { background-size: ${state.width}px ${state.width}px; } .monaco-editor .dirty-diff-added, - .monaco-editor .dirty-diff-added-pattern, .monaco-editor .dirty-diff-modified, - .monaco-editor .dirty-diff-modified-pattern, .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } @@ -282,6 +316,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.onEditorsChanged(); this.onDidActiveEditorChange(); + this.onDidChangeQuickDiffProviders(); this.enabled = true; } @@ -322,8 +357,8 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben const visibleDecorationCount = observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { - const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); - return quickDiffModelRef.object.changes.filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)).length; + const visibleQuickDiffs = quickDiffModelRef.object.quickDiffs.filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); + return quickDiffModelRef.object.changes.filter(change => visibleQuickDiffs.some(quickDiff => quickDiff.id === change.providerId)).length; }); store.add(autorun(reader => { @@ -333,6 +368,43 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben })); } + private onDidChangeQuickDiffProviders(): void { + this.transientDisposables.add(autorunWithStore((reader, store) => { + const providers = this.quickDiffProviders.read(reader); + + const labels: string[] = []; + for (let index = 0; index < providers.length; index++) { + const provider = providers[index]; + if (labels.includes(provider.label)) { + continue; + } + + const visible = this.quickDiffService.isQuickDiffProviderVisible(provider.id); + const group = provider.kind !== 'contributed' ? '0_scm' : '1_contributed'; + const order = index + 1; + + store.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.scm.action.toggleQuickDiffVisibility.${provider.id}`, + title: provider.label, + toggled: visible ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE, + menu: { + id: MenuId.SCMQuickDiffDecorations, group, order + }, + f1: false + }); + } + override run(accessor: ServicesAccessor): void { + const quickDiffService = accessor.get(IQuickDiffService); + quickDiffService.toggleQuickDiffProviderVisibility(provider.id); + } + })); + labels.push(provider.label); + } + })); + } + private onEditorsChanged(): void { for (const editor of this.editorService.visibleTextEditorControls) { if (!isCodeEditor(editor)) { @@ -358,7 +430,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.decorators.set(textModel.uri, new DisposableMap()); } - this.decorators.get(textModel.uri)!.set(editorId, new QuickDiffDecorator(editor, quickDiffModelRef, this.configurationService)); + this.decorators.get(textModel.uri)!.set(editorId, new QuickDiffDecorator(editor, quickDiffModelRef, this.configurationService, this.quickDiffService)); } // Dispose decorators for editors that are no longer visible. diff --git a/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index ce0f83a212f..2a88b551251 100644 --- a/code/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/code/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'; @@ -178,15 +178,14 @@ export class QuickDiffModel extends Disposable { public getQuickDiffResults(): QuickDiffResult[] { return this._quickDiffs.map(quickDiff => { const changes = this.changes - .filter(change => change.label === quickDiff.label); + .filter(change => change.providerId === quickDiff.id); return { - label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, changes: changes.map(change => change.change), changes2: changes.map(change => change.change2) - }; + } satisfies QuickDiffResult; }); } @@ -245,22 +244,52 @@ export class QuickDiffModel extends Disposable { return Promise.resolve({ changes: [], mapChanges: new Map() }); // disposed } - const filteredToDiffable = originalURIs.filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); - if (filteredToDiffable.length === 0) { + const quickDiffs = originalURIs + .filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); + if (quickDiffs.length === 0) { return Promise.resolve({ changes: [], mapChanges: new Map() }); // All files are too large } + const quickDiffPrimary = quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const ignoreTrimWhitespaceSetting = this.configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); const ignoreTrimWhitespace = ignoreTrimWhitespaceSetting === 'inherit' ? this.configurationService.getValue('diffEditor.ignoreTrimWhitespace') : ignoreTrimWhitespaceSetting !== 'false'; const allDiffs: QuickDiffChange[] = []; - for (const quickDiff of filteredToDiffable) { + for (const quickDiff of quickDiffs) { const diff = await this._diff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); if (diff.changes && diff.changes2 && diff.changes.length === diff.changes2.length) { for (let index = 0; index < diff.changes.length; index++) { + const change2 = diff.changes2[index]; + + // The secondary diffs are complimentary to the primary diffs, and + // they overlap. We need to remove the secondary quick diffs that + // overlap with primary quick diffs that are already in the array. + if (quickDiffPrimary && quickDiff.kind === 'secondary') { + // Check whether the: + // 1. the modified line range is equal + // 2. the original line range length is equal + const primaryQuickDiffChange = allDiffs + .find(d => d.change2.modified.equals(change2.modified) && + d.change2.original.length === change2.original.length); + + if (primaryQuickDiffChange) { + // Check whether the original content matches + const primaryModel = this._originalEditorModels.get(quickDiffPrimary.originalResource)?.textEditorModel; + const primaryContent = primaryModel?.getValueInRange(primaryQuickDiffChange.change2.toRangeMapping().originalRange); + + const secondaryModel = this._originalEditorModels.get(quickDiff.originalResource)?.textEditorModel; + const secondaryContent = secondaryModel?.getValueInRange(change2.toRangeMapping().originalRange); + if (primaryContent === secondaryContent) { + continue; + } + } + } + allDiffs.push({ + providerId: quickDiff.id, label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, @@ -270,6 +299,7 @@ export class QuickDiffModel extends Disposable { } } } + const sorted = allDiffs.sort((a, b) => compareChanges(a.change, b.change)); const map: Map = new Map(); for (let i = 0; i < sorted.length; i++) { @@ -309,7 +339,11 @@ export class QuickDiffModel extends Disposable { return []; } - if (equals(this._quickDiffs, quickDiffs, (a, b) => a.originalResource.toString() === b.originalResource.toString() && a.label === b.label)) { + if (equals(this._quickDiffs, quickDiffs, (a, b) => + a.id === b.id && + a.originalResource.toString() === b.originalResource.toString() && + this.quickDiffService.isQuickDiffProviderVisible(a.id) === this.quickDiffService.isQuickDiffProviderVisible(b.id)) + ) { return quickDiffs; } @@ -358,7 +392,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([]); } @@ -368,44 +402,38 @@ export class QuickDiffModel extends Disposable { } findNextClosestChange(lineNumber: number, inclusive = true, provider?: string): number { - let preferredProvider: string | undefined; - if (!provider && inclusive) { - preferredProvider = this.quickDiffs.find(value => value.isSCM)?.label; + const visibleQuickDiffLabels = this.quickDiffs + .filter(quickDiff => (!provider || quickDiff.label === provider) && + this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) + .map(quickDiff => quickDiff.label); + + if (!inclusive) { + // Next visible change + const nextChange = this.changes + .findIndex(change => visibleQuickDiffLabels.includes(change.label) && + change.change.modifiedStartLineNumber > lineNumber); + + return nextChange !== -1 ? nextChange : 0; } - const possibleChanges: number[] = []; - for (let i = 0; i < this.changes.length; i++) { - if (provider && this.changes[i].label !== provider) { - continue; - } + const primaryQuickDiffId = this.quickDiffs + .find(quickDiff => quickDiff.kind === 'primary')?.id; - // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.label === this.changes[i].label)?.visible) { - continue; - } + const primaryInclusiveChangeIndex = this.changes + .findIndex(change => change.providerId === primaryQuickDiffId && + change.change.modifiedStartLineNumber <= lineNumber && + getModifiedEndLineNumber(change.change) >= lineNumber); - const change = this.changes[i]; - const possibleChangesLength = possibleChanges.length; - - if (inclusive) { - if (getModifiedEndLineNumber(change.change) >= lineNumber) { - if (preferredProvider && change.label !== preferredProvider) { - possibleChanges.push(i); - } else { - return i; - } - } - } else { - if (change.change.modifiedStartLineNumber > lineNumber) { - return i; - } - } - if ((possibleChanges.length > 0) && (possibleChanges.length === possibleChangesLength)) { - return possibleChanges[0]; - } + if (primaryInclusiveChangeIndex !== -1) { + return primaryInclusiveChangeIndex; } - return possibleChanges.length > 0 ? possibleChanges[0] : 0; + const inclusiveChangeIndex = this.changes + .findIndex(change => visibleQuickDiffLabels.includes(change.label) && + change.change.modifiedStartLineNumber <= lineNumber && + getModifiedEndLineNumber(change.change) >= lineNumber); + + return inclusiveChangeIndex !== -1 ? inclusiveChangeIndex : 0; } findPreviousClosestChange(lineNumber: number, inclusive = true, provider?: string): number { @@ -415,7 +443,8 @@ export class QuickDiffModel extends Disposable { } // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.label === this.changes[i].label)?.visible) { + const quickDiff = this.quickDiffs.find(quickDiff => quickDiff.id === this.changes[i].providerId); + if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { continue; } diff --git a/code/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/code/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index f22648c7169..1c6bebfe09b 100644 --- a/code/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/code/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -28,7 +28,7 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { rot } from '../../../../base/common/numbers.js'; import { ISplice } from '../../../../base/common/sequence.js'; -import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, lineIntersectsChange, QuickDiffChange } from '../common/quickDiff.js'; +import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, IQuickDiffService, lineIntersectsChange, QuickDiff, QuickDiffChange } from '../common/quickDiff.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -58,33 +58,26 @@ export interface IQuickDiffSelectItem extends ISelectOptionItem { } export class QuickDiffPickerViewItem extends SelectActionViewItem { - private readonly optionsItems: IQuickDiffSelectItem[]; + private optionsItems: IQuickDiffSelectItem[] = []; constructor( action: IAction, - providers: string[], - selected: string, @IContextViewService contextViewService: IContextViewService, @IThemeService themeService: IThemeService ) { - const items = providers.map(provider => ({ provider, text: provider })); - let startingSelection = providers.indexOf(selected); - if (startingSelection === -1) { - startingSelection = 0; - } const styles = { ...defaultSelectBoxStyles }; const theme = themeService.getColorTheme(); const editorBackgroundColor = theme.getColor(editorBackground); const peekTitleColor = theme.getColor(peekViewTitleBackground); const opaqueTitleColor = peekTitleColor?.makeOpaque(editorBackgroundColor!) ?? editorBackgroundColor!; styles.selectBackground = opaqueTitleColor.lighten(.6).toString(); - super(null, action, items, startingSelection, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); - this.optionsItems = items; + super(null, action, [], 0, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); } - public setSelection(provider: string) { + public setSelection(providers: string[], provider: string) { + this.optionsItems = providers.map(provider => ({ provider, text: provider })); const index = this.optionsItems.findIndex(item => item.provider === provider); - this.select(index); + this.setOptions(this.optionsItems, index); } protected override getActionContext(_: string, index: number): IQuickDiffSelectItem { @@ -168,7 +161,8 @@ class QuickDiffWidget extends PeekViewWidget { @IThemeService private readonly themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private contextKeyService: IContextKeyService + @IContextKeyService private contextKeyService: IContextKeyService, + @IQuickDiffService private readonly quickDiffService: IQuickDiffService ) { super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' }, instantiationService); @@ -207,6 +201,7 @@ class QuickDiffWidget extends PeekViewWidget { const labeledChange = this.model.changes[index]; const change = labeledChange.change; this._index = index; + this.contextKeyService.createKey('originalResource', this.model.changes[index].original.toString()); this.contextKeyService.createKey('originalResourceScheme', this.model.changes[index].original.scheme); this.updateActions(); @@ -228,7 +223,6 @@ class QuickDiffWidget extends PeekViewWidget { return; } this.diffEditor.setModel(diffEditorModel); - this.dropdown?.setSelection(labeledChange.label); const position = new Position(getModifiedEndLineNumber(change), 1); @@ -237,6 +231,7 @@ class QuickDiffWidget extends PeekViewWidget { const editorHeightInLines = Math.floor(editorHeight / lineHeight); const height = Math.min(getChangeHeight(change) + /* padding */ 8, Math.floor(editorHeightInLines / 3)); + this.updateDropdown(labeledChange.label); this.renderTitle(labeledChange.label); const changeType = getChangeType(change); @@ -294,7 +289,7 @@ class QuickDiffWidget extends PeekViewWidget { } } let closestLesserIndex = this._index > 0 ? this._index - 1 : this.model.changes.length - 1; - for (let i = closestLesserIndex; i !== this._index; i >= 0 ? i-- : i = this.model.changes.length - 1) { + for (let i = closestLesserIndex; i !== this._index; i > 0 ? i-- : i = this.model.changes.length - 1) { if (this.model.changes[i].label === newProvider) { closestLesserIndex = i; break; @@ -307,12 +302,8 @@ class QuickDiffWidget extends PeekViewWidget { } private shouldUseDropdown(): boolean { - const visibleQuickDiffs = this.model.quickDiffs.filter(quickDiff => quickDiff.visible); - const visibleQuickDiffResults = this.model.getQuickDiffResults() - .filter(result => visibleQuickDiffs.some(quickDiff => quickDiff.label === result.label)); - - return visibleQuickDiffResults - .filter(quickDiff => quickDiff.changes.length > 0).length > 1; + const quickDiffs = this.getQuickDiffsContainingChange(); + return quickDiffs.length > 1; } private updateActions(): void { @@ -336,17 +327,31 @@ class QuickDiffWidget extends PeekViewWidget { this._actionbarWidget.push(this._disposables.add(new Action('peekview.close', nls.localize('label.close', "Close"), ThemeIcon.asClassName(Codicon.close), true, () => this.dispose())), { label: false, icon: true }); } + private updateDropdown(label: string): void { + const quickDiffs = this.getQuickDiffsContainingChange(); + this.dropdown?.setSelection(quickDiffs.map(quickDiff => quickDiff.label), label); + } + + private getQuickDiffsContainingChange(): QuickDiff[] { + const change = this.model.changes[this._index]; + + const quickDiffsWithChange = this.model.changes + .filter(c => change.change2.modified.overlapOrTouch(c.change2.modified)) + .map(c => c.providerId); + + return this.model.quickDiffs + .filter(quickDiff => quickDiffsWithChange.includes(quickDiff.id) && + this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)); + } + protected override _fillHead(container: HTMLElement): void { super._fillHead(container, true); - const visibleQuickDiffs = this.model.quickDiffs.filter(quickDiff => quickDiff.visible); - + // Render an empty picker which will be populated later this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, - new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)), - visibleQuickDiffs.map(quickDiff => quickDiff.label), this.model.changes[this._index].label); + new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event))); this.dropdown.render(this.dropdownContainer); - this.updateActions(); } protected override _getActionBarOptions(): IActionBarOptions { diff --git a/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index a3ce54d97ca..22199e56e6d 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -626,6 +626,15 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +MenuRegistry.appendMenuItem(MenuId.EditorLineNumberContext, { + title: localize('quickDiffDecoration', "Diff Decorations"), + submenu: MenuId.SCMQuickDiffDecorations, + when: ContextKeyExpr.or( + ContextKeyExpr.equals('config.scm.diffDecorations', 'all'), + ContextKeyExpr.equals('config.scm.diffDecorations', 'gutter')), + group: '9_quickDiffDecorations' +}); + registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index fdde018b017..75ca8982fcd 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -497,7 +497,7 @@ class HistoryItemRenderer implements ITreeRenderer('quickDiff'); @@ -25,13 +26,24 @@ const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackg dark: '#1B81A8', light: '#2090D3', hcDark: '#1B81A8', hcLight: '#2090D3' }, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); +registerColor('editorGutter.modifiedSecondaryBackground', + { dark: darken(editorGutterModifiedBackground, 0.5), light: lighten(editorGutterModifiedBackground, 0.7), hcDark: '#1B81A8', hcLight: '#2090D3' }, + nls.localize('editorGutterModifiedSecondaryBackground', "Editor gutter secondary background color for lines that are modified.")); + const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { dark: '#487E02', light: '#48985D', hcDark: '#487E02', hcLight: '#48985D' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); +registerColor('editorGutter.addedSecondaryBackground', + { dark: darken(editorGutterAddedBackground, 0.5), light: lighten(editorGutterAddedBackground, 0.7), hcDark: '#487E02', hcLight: '#48985D' }, + nls.localize('editorGutterAddedSecondaryBackground', "Editor gutter secondary background color for lines that are added.")); + const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); +registerColor('editorGutter.deletedSecondaryBackground', + { dark: darken(editorGutterDeletedBackground, 0.4), light: lighten(editorGutterDeletedBackground, 0.3), hcDark: '#F48771', hcLight: '#B5200D' }, + nls.localize('editorGutterDeletedSecondaryBackground', "Editor gutter secondary background color for lines that are deleted.")); export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); @@ -55,22 +67,23 @@ export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemG export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); export interface QuickDiffProvider { - label: string; - rootUri: URI | undefined; - selector?: LanguageSelector; - isSCM: boolean; - visible: boolean; + readonly id: string; + readonly label: string; + readonly rootUri: URI | undefined; + readonly selector?: LanguageSelector; + readonly kind: 'primary' | 'secondary' | 'contributed'; getOriginalResource(uri: URI): Promise; } export interface QuickDiff { - label: string; - originalResource: URI; - isSCM: boolean; - visible: boolean; + readonly id: string; + readonly label: string; + readonly originalResource: URI; + readonly kind: 'primary' | 'secondary' | 'contributed'; } export interface QuickDiffChange { + readonly providerId: string; readonly label: string; readonly original: URI; readonly modified: URI; @@ -79,7 +92,6 @@ export interface QuickDiffChange { } export interface QuickDiffResult { - readonly label: string; readonly original: URI; readonly modified: URI; readonly changes: IChange[]; @@ -90,8 +102,11 @@ export interface IQuickDiffService { readonly _serviceBrand: undefined; readonly onDidChangeQuickDiffProviders: Event; + readonly providers: readonly QuickDiffProvider[]; addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable; getQuickDiffs(uri: URI, language?: string, isSynchronized?: boolean): Promise; + toggleQuickDiffProviderVisibility(id: string): void; + isQuickDiffProviderVisible(id: string): boolean; } export enum ChangeType { diff --git a/code/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/code/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 5b872ae5722..60ba18a90cf 100644 --- a/code/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/code/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -10,6 +10,7 @@ import { isEqualOrParent } from '../../../../base/common/resources.js'; import { score } from '../../../../editor/common/languageSelector.js'; import { Emitter } from '../../../../base/common/event.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffProvider) => number { return (a, b) => { @@ -25,7 +26,7 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr const bIsParent = isEqualOrParent(uri, b.rootUri!); if (aIsParent && bIsParent) { - return a.rootUri!.fsPath.length - b.rootUri!.fsPath.length; + return providerComparer(a, b); } else if (aIsParent) { return -1; } else if (bIsParent) { @@ -36,15 +37,40 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr }; } +function providerComparer(a: QuickDiffProvider, b: QuickDiffProvider): number { + if (a.kind === 'primary') { + return -1; + } else if (b.kind === 'primary') { + return 1; + } else if (a.kind === 'secondary') { + return -1; + } else if (b.kind === 'secondary') { + return 1; + } + return 0; +} + export class QuickDiffService extends Disposable implements IQuickDiffService { declare readonly _serviceBrand: undefined; + private static readonly STORAGE_KEY = 'workbench.scm.quickDiffProviders.hidden'; private quickDiffProviders: Set = new Set(); + get providers(): readonly QuickDiffProvider[] { + return Array.from(this.quickDiffProviders).sort(providerComparer); + } + private readonly _onDidChangeQuickDiffProviders = this._register(new Emitter()); readonly onDidChangeQuickDiffProviders = this._onDidChangeQuickDiffProviders.event; - constructor(@IUriIdentityService private readonly uriIdentityService: IUriIdentityService) { + private hiddenQuickDiffProviders = new Set(); + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + ) { super(); + + this.loadState(); } addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable { @@ -58,26 +84,64 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { }; } - private isQuickDiff(diff: { originalResource?: URI; label?: string; isSCM?: boolean }): diff is QuickDiff { - return !!diff.originalResource && (typeof diff.label === 'string') && (typeof diff.isSCM === 'boolean'); - } - async getQuickDiffs(uri: URI, language: string = '', isSynchronized: boolean = false): Promise { const providers = Array.from(this.quickDiffProviders) .filter(provider => !provider.rootUri || this.uriIdentityService.extUri.isEqualOrParent(uri, provider.rootUri)) .sort(createProviderComparer(uri)); - const diffs = await Promise.all(providers.map(async provider => { + const quickDiffOriginalResources = await Promise.all(providers.map(async provider => { const scoreValue = provider.selector ? score(provider.selector, uri, language, isSynchronized, undefined, undefined) : 10; - const diff: Partial = { - originalResource: scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined, - label: provider.label, - isSCM: provider.isSCM, - visible: provider.visible - }; - return diff; + const originalResource = scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined; + return { provider, originalResource }; })); - return diffs.filter(this.isQuickDiff); + + const quickDiffs: QuickDiff[] = []; + for (const { provider, originalResource } of quickDiffOriginalResources) { + if (!originalResource) { + continue; + } + + quickDiffs.push({ + id: provider.id, + label: provider.label, + kind: provider.kind, + originalResource, + } satisfies QuickDiff); + } + + return quickDiffs; + } + + toggleQuickDiffProviderVisibility(id: string): void { + if (this.isQuickDiffProviderVisible(id)) { + this.hiddenQuickDiffProviders.add(id); + } else { + this.hiddenQuickDiffProviders.delete(id); + } + + this.saveState(); + this._onDidChangeQuickDiffProviders.fire(); + } + + isQuickDiffProviderVisible(id: string): boolean { + return !this.hiddenQuickDiffProviders.has(id); + } + + private loadState(): void { + const raw = this.storageService.get(QuickDiffService.STORAGE_KEY, StorageScope.PROFILE); + if (raw) { + try { + this.hiddenQuickDiffProviders = new Set(JSON.parse(raw)); + } catch { } + } + } + + private saveState(): void { + if (this.hiddenQuickDiffProviders.size === 0) { + this.storageService.remove(QuickDiffService.STORAGE_KEY, StorageScope.PROFILE); + } else { + this.storageService.store(QuickDiffService.STORAGE_KEY, JSON.stringify(Array.from(this.hiddenQuickDiffProviders)), StorageScope.PROFILE, StorageTarget.USER); + } } } diff --git a/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index bcb65cdddfd..acbecc9773b 100644 --- a/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/code/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -71,6 +71,28 @@ function isEditorSymbolQuickPickItem(pick?: IAnythingQuickPickItem): pick is IEd return !!candidate?.range && !!candidate.resource; } +interface IAnythingPickState extends IDisposable { + picker: IQuickPick | undefined; + editorViewState: PickerEditorState; + + scorerCache: FuzzyScorerCache; + fileQueryCache: FileQueryCacheState | undefined; + + lastOriginalFilter: string | undefined; + lastFilter: string | undefined; + lastRange: IRange | undefined; + + lastGlobalPicks: PicksWithActive | undefined; + + isQuickNavigating: boolean | undefined; + + /** + * Sets the picker for this pick state. + */ + set(picker: IQuickPick): void; +} + + export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = ''; @@ -85,56 +107,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; - - editorViewState = this._register(this.instantiationService.createInstance(PickerEditorState)); - - scorerCache: FuzzyScorerCache = Object.create(null); - fileQueryCache: FileQueryCacheState | undefined = undefined; - - lastOriginalFilter: string | undefined = undefined; - lastFilter: string | undefined = undefined; - lastRange: IRange | undefined = undefined; - - lastGlobalPicks: PicksWithActive | undefined = undefined; - - isQuickNavigating: boolean | undefined = undefined; - - constructor( - private readonly provider: AnythingQuickAccessProvider, - private readonly instantiationService: IInstantiationService - ) { - super(); - } - - set(picker: IQuickPick): void { - - // Picker for this run - this.picker = picker; - Event.once(picker.onDispose)(() => { - if (picker === this.picker) { - this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long - } - }); - - // Caches - const isQuickNavigating = !!picker.quickNavigate; - if (!isQuickNavigating) { - this.fileQueryCache = this.provider.createFileQueryCache(); - this.scorerCache = Object.create(null); - } - - // Other - this.isQuickNavigating = isQuickNavigating; - this.lastOriginalFilter = undefined; - this.lastFilter = undefined; - this.lastRange = undefined; - this.lastGlobalPicks = undefined; - this.editorViewState.reset(); - } - }(this, this.instantiationService)); + private readonly pickState: IAnythingPickState; get defaultFilterValue(): DefaultQuickAccessFilterValue | undefined { if (this.configuration.preserveInput) { @@ -171,6 +144,62 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | undefined = undefined; + + editorViewState: PickerEditorState; + + scorerCache: FuzzyScorerCache = Object.create(null); + fileQueryCache: FileQueryCacheState | undefined = undefined; + + lastOriginalFilter: string | undefined = undefined; + lastFilter: string | undefined = undefined; + lastRange: IRange | undefined = undefined; + + lastGlobalPicks: PicksWithActive | undefined = undefined; + + isQuickNavigating: boolean | undefined = undefined; + + constructor( + private readonly provider: AnythingQuickAccessProvider, + instantiationService: IInstantiationService + ) { + super(); + this.editorViewState = this._register(instantiationService.createInstance(PickerEditorState)); + } + + set(picker: IQuickPick): void { + + // Picker for this run + this.picker = picker; + Event.once(picker.onDispose)(() => { + if (picker === this.picker) { + this.picker = undefined; // clear the picker when disposed to not keep it in memory for too long + } + }); + + // Caches + const isQuickNavigating = !!picker.quickNavigate; + if (!isQuickNavigating) { + this.fileQueryCache = this.provider.createFileQueryCache(); + this.scorerCache = Object.create(null); + } + + // Other + this.isQuickNavigating = isQuickNavigating; + this.lastOriginalFilter = undefined; + this.lastFilter = undefined; + this.lastRange = undefined; + this.lastGlobalPicks = undefined; + this.editorViewState.reset(); + } + }(this, instantiationService)); + + this.fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); + this.workspaceSymbolsQuickAccess = this._register(instantiationService.createInstance(SymbolsQuickAccessProvider)); + this.editorSymbolsQuickAccess = this.instantiationService.createInstance(GotoSymbolQuickAccessProvider); } private get configuration() { @@ -519,7 +548,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); - private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); + private readonly fileQueryBuilder: QueryBuilder; private createFileQueryCache(): FileQueryCacheState { return new FileQueryCacheState( @@ -825,7 +854,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { if ( @@ -850,7 +879,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> | null { const filterSegments = query.original.split(GotoSymbolQuickAccessProvider.PREFIX); diff --git a/code/src/vs/workbench/contrib/search/browser/media/searchview.css b/code/src/vs/workbench/contrib/search/browser/media/searchview.css index ddab0a2e10b..000d9a09f49 100644 --- a/code/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/code/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -169,6 +169,18 @@ overflow-wrap: break-word; } +.search-view .message.ai-keywords { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + margin: 0 22px 8px; + padding: 0px; +} + .search-view .message p:first-child { margin-top: 0px; margin-bottom: 0px; @@ -186,6 +198,18 @@ color: var(--vscode-textLink-activeForeground); } +.search-view .message .keyword-refresh { + vertical-align: sub; + margin-right: 4px; + cursor: pointer; +} + +.search-view .message .keyword-refresh:hover, +.search-view .message .keyword-refresh:active { + color: var(--vscode-textLink-activeForeground); +} + + .search-view .foldermatch, .search-view .filematch { display: flex; diff --git a/code/src/vs/workbench/contrib/search/browser/searchView.ts b/code/src/vs/workbench/contrib/search/browser/searchView.ts index 2a7f4d3c326..94ca7851e95 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchView.ts @@ -72,7 +72,7 @@ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/edit import { IPreferencesService, ISettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; import { ITextQueryBuilderOptions, QueryBuilder } from '../../../services/search/common/queryBuilder.js'; import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; -import { TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; +import { AISearchKeyword, TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -84,6 +84,7 @@ import { ISearchTreeMatch, isSearchTreeMatch, RenderableMatch, SearchModelLocati import { INotebookFileInstanceMatch, isIMatchInNotebook } from './notebookSearch/notebookSearchModelBase.js'; import { searchMatchComparer } from './searchCompare.js'; import { AIFolderMatchWorkspaceRootImpl } from './AISearch/aiSearchModel.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; const $ = dom.$; @@ -200,7 +201,8 @@ export class SearchView extends ViewPane { @IHoverService hoverService: IHoverService, @INotebookService private readonly notebookService: INotebookService, @ILogService private readonly logService: ILogService, - @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -1820,7 +1822,11 @@ export class SearchView extends ViewPane { const result = this.viewModel.addAIResults(); return result.then((complete) => { clearTimeout(slowTimer); - this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); + if (complete.aiKeywords && complete.aiKeywords.length > 0) { + this.updateKeywordSuggestion(complete.aiKeywords); + } else { + this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); + } return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete, false); }, (e) => { clearTimeout(slowTimer); @@ -1948,6 +1954,52 @@ export class SearchView extends ViewPane { } } + private handleKeywordClick(keyword: string, index: number, maxKeywords: number) { + this.searchWidget.searchInput?.setValue(keyword); + this.triggerQueryChange({ preserveFocus: false, triggeredOnType: false, shouldKeepAIResults: false }); + type KeywordClickClassification = { + owner: 'osortega'; + comment: 'Fired when the user clicks on a keyword suggestion'; + index: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The index of the keyword clicked' }; + maxKeywords: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The total number of suggested keywords' }; + }; + type KeywordClickEvent = { + index: number; + maxKeywords: number; + }; + this.telemetryService.publicLog2('searchKeywordClick', { + index, + maxKeywords + }); + } + + private updateKeywordSuggestion(keywords: AISearchKeyword[]) { + const messageEl = this.clearMessage(); + messageEl.classList.add('ai-keywords'); + + if (keywords.length === 0) { + // Do not display anything if there are no keywords + return; + } + + // Add unclickable message + const resultMsg = nls.localize('keywordSuggestion.message', "Search instead for: "); + dom.append(messageEl, resultMsg); + + const topKeywords = keywords.slice(0, 3); + topKeywords.forEach((keyword, index) => { + if (index > 0 && index < topKeywords.length) { + dom.append(messageEl, ', '); + } + const button = this.messageDisposables.add(new SearchLinkButton( + keyword.keyword, + () => this.handleKeywordClick(keyword.keyword, index, topKeywords.length), + this.hoverService + )); + dom.append(messageEl, button.element); + }); + } + private addMessage(message: TextSearchCompleteMessage) { const messageBox = this.messagesElement.firstChild as HTMLDivElement; if (!messageBox) { return; } diff --git a/code/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/code/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index e11f2203cda..84a3c98a421 100644 --- a/code/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/code/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -129,8 +129,6 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider('shareProviderCount', 0, localize('shareProviderCount', "The number of available share providers")); @@ -89,6 +89,6 @@ export class ShareService implements IShareService { registerAction2(class ToggleShareControl extends ToggleTitleBarConfigAction { constructor() { - super('workbench.experimental.share.enabled', localize('toggle.share', 'Share'), localize('toggle.shareDescription', "Toggle visibility of the Share action in title bar"), 3, false, ContextKeyExpr.and(ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.and(ShareProviderCountContext.notEqualsTo(0), WorkspaceFolderCountContext.notEqualsTo(0)))); + super('workbench.experimental.share.enabled', localize('toggle.share', 'Share'), localize('toggle.shareDescription', "Toggle visibility of the Share action in title bar"), 3, ContextKeyExpr.and(IsCompactTitleBarContext.toNegated(), ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.and(ShareProviderCountContext.notEqualsTo(0), WorkspaceFolderCountContext.notEqualsTo(0)))); } }); diff --git a/code/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/code/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index e7df0b22037..a69a499fcb5 100644 --- a/code/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/code/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IProductService } from '../../../../platform/product/common/productService.js'; import { ISurveyData } from '../../../../base/common/product.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { Severity, INotificationService } from '../../../../platform/notification/common/notification.js'; +import { Severity, INotificationService, NotificationPriority } from '../../../../platform/notification/common/notification.js'; import { ITextFileService, ITextFileEditorModel } from '../../../services/textfile/common/textfiles.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; @@ -119,7 +119,7 @@ class LanguageSurvey extends Disposable { storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); } }], - { sticky: true } + { sticky: true, priority: NotificationPriority.OPTIONAL } ); } } diff --git a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 25385286aad..78c67ee4a15 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2249,7 +2249,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer configuringTask.configures.type, JSON.stringify(configuringTask._source.config.element, undefined, 4) )); - this._showOutput(); } }); diff --git a/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 2fcb98375e2..96bd7ef8e9a 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/code/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]) { @@ -843,7 +842,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (eventCounter === 0) { if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) { - this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); + // this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); const reveal = task.command.presentation!.reveal; const revealProblems = task.command.presentation!.revealProblems; if (revealProblems === RevealProblemKind.OnProblem) { @@ -853,7 +852,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._terminalGroupService.showPanel(false); } } else { - this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); + // this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); } } } @@ -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/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 15ffde581b3..a93582cc1eb 100644 --- a/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/code/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -39,6 +39,7 @@ import { localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IOutputService } from '../../../services/output/common/output.js'; import { ILoggerResource, ILoggerService, LogLevel } from '../../../../platform/log/common/log.js'; +import { VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; type TelemetryData = { mimeType: TelemetryTrustedValue; @@ -382,7 +383,7 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc }>('window.titleBarStyle', { settingValue: this.getValueToReport(key, target), source }); return; - case 'extensions.verifySignature': + case VerifyExtensionSignatureConfigKey: this.telemetryService.publicLog2; - runCommand(command: string, shouldExecute?: boolean): void; + runCommand(command: string, shouldExecute?: boolean): Promise; /** * Takes a path and returns the properly escaped path to send to a given shell. On Windows, this diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4b1bd172274..a52955bc4a8 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/code/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'; @@ -194,6 +194,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; @@ -484,6 +486,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (commandDetection) { commandDetection.promptInputModel.setShellType(this.shellType); capabilityListeners.set(capability, Event.any( + commandDetection.onPromptTypeChanged, commandDetection.promptInputModel.onDidStartInput, commandDetection.promptInputModel.onDidChangeInput, commandDetection.promptInputModel.onDidFinishInput @@ -1469,6 +1472,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/code/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts index 6823a176622..e321b696b4b 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts +++ b/code/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/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index 3c232558048..9874708eb42 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -96,6 +96,10 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { if (seenSequences.length > 0) { detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); } + const promptType = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptType; + if (promptType) { + detailedAdditions.push(`Prompt type: \`${promptType}\``); + } const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString(); if (combinedString !== undefined) { detailedAdditions.push(`Prompt input: \`${combinedString}\``); diff --git a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index b835c0b5c52..44c8a117b9b 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -225,6 +225,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach fastScrollModifier: 'alt', fastScrollSensitivity: config.fastScrollSensitivity, scrollSensitivity: config.mouseWheelScrollSensitivity, + scrollOnEraseInDisplay: true, wordSeparator: config.wordSeparators, overviewRuler: { width: 14, diff --git a/code/src/vs/workbench/contrib/terminal/common/basePty.ts b/code/src/vs/workbench/contrib/terminal/common/basePty.ts index 9a8c11ce3ff..a851f56225e 100644 --- a/code/src/vs/workbench/contrib/terminal/common/basePty.ts +++ b/code/src/vs/workbench/contrib/terminal/common/basePty.ts @@ -25,7 +25,8 @@ export abstract class BasePty extends Disposable implements Partial= 4 )); then use_associative_array=1 # Associative arrays are only available in bash 4.0+ @@ -198,6 +201,12 @@ if [ "$__vsc_stable" = "0" ]; then builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" fi +if [ -n "$STARSHIP_SESSION_KEY" ]; then + builtin printf '\e]633;P;PromptType=starship\a' +elif [ -n "$POSH_SESSION_ID" ]; then + builtin printf '\e]633;P;PromptType=oh-my-posh\a' +fi + # Report this shell supports rich command detection builtin printf '\e]633;P;HasRichCommandDetection=True\a' @@ -242,26 +251,6 @@ __updateEnvCacheAA() { fi } -__trackMissingEnvVarsAA() { - if [ "$use_associative_array" = 1 ]; then - declare -A currentEnvMap - while IFS= read -r line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" - currentEnvMap["$key"]="$value" - fi - done < <(env) - - for key in "${!vsc_aa_env[@]}"; do - if [ -z "${currentEnvMap[$key]}" ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "$key" "$(__vsc_escape_value "${vsc_aa_env[$key]}")" "$__vsc_nonce" - builtin unset "vsc_aa_env[$key]" - fi - done - fi -} - __updateEnvCache() { local key="$1" local value="$2" @@ -281,86 +270,51 @@ __updateEnvCache() { builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" } -__trackMissingEnvVars() { - local current_env_keys=() - - while IFS='=' read -r key value; do - current_env_keys+=("$key") - done < <(env) - - # Compare vsc_env_keys with user's current_env_keys - for key in "${vsc_env_keys[@]}"; do - local found=0 - for env_key in "${current_env_keys[@]}"; do - if [[ "$key" == "$env_key" ]]; then - found=1 - break - fi - done - if [ "$found" = 0 ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${vsc_env_keys[i]}" "$(__vsc_escape_value "${vsc_env_values[i]}")" "$__vsc_nonce" - builtin unset 'vsc_env_keys[i]' - builtin unset 'vsc_env_values[i]' - fi - done - - # Remove gaps from unset - vsc_env_keys=("${vsc_env_keys[@]}") - vsc_env_values=("${vsc_env_values[@]}") -} - __vsc_update_env() { - if [[ "$__vscode_shell_env_reporting" == "1" ]]; then + if [[ ${#envVarsToReport[@]} -gt 0 ]]; then builtin printf '\e]633;EnvSingleStart;%s;%s\a' 0 $__vsc_nonce if [ "$use_associative_array" = 1 ]; then if [ ${#vsc_aa_env[@]} -eq 0 ]; then # Associative array is empty, do not diff, just add - # Use null byte instead of a newline to support multi-line values (e.g. PS1 values) - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - # %% removes longest match of =* Ensure we get everything before first equal sign. - local key="${line%%=*}" - # # removes shortest match of *= Ensure we get everything after first equal sign. Preserving additional equal signs. - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" vsc_aa_env["$key"]="$value" builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" fi - done < <(env -0) # env command with null bytes as separator instead of newlines + done else # Diff approach for associative array - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" __updateEnvCacheAA "$key" "$value" fi - done < <(env -0) - __trackMissingEnvVarsAA + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi else if [[ -z ${vsc_env_keys[@]} ]] && [[ -z ${vsc_env_values[@]} ]]; then # Non associative arrays are both empty, do not diff, just add - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" vsc_env_keys+=("$key") vsc_env_values+=("$value") builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" fi - done < <(env -0) + done else # Diff approach for non-associative arrays - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" __updateEnvCache "$key" "$value" fi - done < <(env -0) - __trackMissingEnvVars + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi fi builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 62b93ad900e..74536cbd328 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -113,8 +113,20 @@ unset VSCODE_NONCE __vscode_shell_env_reporting="$VSCODE_SHELL_ENV_REPORTING" unset VSCODE_SHELL_ENV_REPORTING +envVarsToReport=() +IFS=',' read -rA envVarsToReport <<< "$__vscode_shell_env_reporting" + builtin printf "\e]633;P;ContinuationPrompt=%s\a" "$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')" +# Report prompt type +if [ -n "$ZSH" ] && [ -n "$ZSH_VERSION" ] && (( ${+functions[omz]} )) ; then + builtin printf '\e]633;P;PromptType=oh-my-zsh\a' +elif [ -n "$STARSHIP_SESSION_KEY" ]; then + builtin printf '\e]633;P;PromptType=starship\a' +elif [ -n "$P9K_SSH" ] || [ -n "$P9K_TTY" ]; then + builtin printf '\e]633;P;PromptType=p10k\a' +fi + # Report this shell supports rich command detection builtin printf '\e]633;P;HasRichCommandDetection=True\a' @@ -141,23 +153,6 @@ __update_env_cache_aa() { fi } -__track_missing_env_vars_aa() { - if [ $__vsc_use_aa -eq 1 ]; then - typeset -A currentEnvMap - while IFS='=' read -r key value; do - currentEnvMap["$key"]="$value" - done < <(env) - - for k in "${(@k)vsc_aa_env}"; do - # if currentEnvMap does not have the key, then it is missing - if ! [[ -v currentEnvMap[$k] ]]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${(Q)k}" "$(__vsc_escape_value "${vsc_aa_env[$k]}")" "$__vsc_nonce" - builtin unset "vsc_aa_env[$k]" - fi - done - fi -} - __update_env_cache() { local key="$1" local value="$2" @@ -178,69 +173,49 @@ __update_env_cache() { builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" } -__track_missing_env_vars() { - local currentEnvKeys=(); - - while IFS='=' read -r key value; do - currentEnvKeys+=("$key"); - done < <(env); - - # Compare __vsc_env_keys with user's currentEnvKeys - for ((i = 1; i <= ${#__vsc_env_keys[@]}; i++)); do - local found=0; - for envKey in "${currentEnvKeys[@]}"; do - if [[ "${__vsc_env_keys[$i]}" == "$envKey" ]]; then - found=1; - break; - fi; - done; - if [ "$found" = 0 ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${__vsc_env_keys[$i]}" "$(__vsc_escape_value "${__vsc_env_values[$i]}")" "$__vsc_nonce"; - unset "__vsc_env_keys[$i]"; - unset "__vsc_env_values[$i]"; - fi; - done; - - # Remove gaps from unset - __vsc_env_keys=("${(@)__vsc_env_keys}"); - __vsc_env_values=("${(@)__vsc_env_values}"); -} - - __vsc_update_env() { - if [[ "$__vscode_shell_env_reporting" == "1" ]]; then + if [[ ${#envVarsToReport[@]} -gt 0 ]]; then builtin printf '\e]633;EnvSingleStart;%s;%s;\a' 0 $__vsc_nonce if [ $__vsc_use_aa -eq 1 ]; then if [[ ${#vsc_aa_env[@]} -eq 0 ]]; then # Associative array is empty, do not diff, just add - while IFS='=' read -r key value; do - vsc_aa_env["$key"]="$value" - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + for key in "${envVarsToReport[@]}"; do + if [[ -v $key ]]; then + vsc_aa_env["$key"]="${(P)key}" + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "${(P)key}")" "$__vsc_nonce" + fi + done else # Diff approach for associative array - while IFS='=' read -r key value; do - __update_env_cache_aa "$key" "$value" - done < <(env) - __track_missing_env_vars_aa - + for var in "${envVarsToReport[@]}"; do + if [[ -v $var ]]; then + value="${(P)var}" + __update_env_cache_aa "$var" "$value" + fi + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi else # Two arrays approach if [[ ${#__vsc_env_keys[@]} -eq 0 ]] && [[ ${#__vsc_env_values[@]} -eq 0 ]]; then # Non-associative arrays are both empty, do not diff, just add - while IFS='=' read -r key value; do - __vsc_env_keys+=("$key") - __vsc_env_values+=("$value") - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + for key in "${envVarsToReport[@]}"; do + if [[ -v $key ]]; then + value="${(P)key}" + __vsc_env_keys+=("$key") + __vsc_env_values+=("$value") + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" + fi + done else # Diff approach for non-associative arrays - while IFS='=' read -r key value; do - __update_env_cache "$key" "$value" - done < <(env) - __track_missing_env_vars - + for var in "${envVarsToReport[@]}"; do + if [[ -v $var ]]; then + value="${(P)var}" + __update_env_cache "$var" "$value" + fi + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi fi diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index f486869be8e..9df1ecd8a3f 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -23,6 +23,10 @@ or exit set --global VSCODE_SHELL_INTEGRATION 1 set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING set -e VSCODE_SHELL_ENV_REPORTING +set -g envVarsToReport +if test -n "$__vscode_shell_env_reporting" + set envVarsToReport (string split "," "$__vscode_shell_env_reporting") +end # Apply any explicit path prefix (see #99878) # On fish, '$fish_user_paths' is always prepended to the PATH, for both login and non-login shells, so we need @@ -159,15 +163,20 @@ function __vsc_update_cwd --on-event fish_prompt end end -if test "$__vscode_shell_env_reporting" = "1" +if test -n "$__vscode_shell_env_reporting" function __vsc_update_env --on-event fish_prompt - __vsc_esc EnvSingleStart 1 - for line in (env) - set myVar (echo $line | awk -F= '{print $1}') - set myVal (echo $line | awk -F= '{print $2}') - __vsc_esc EnvSingleEntry $myVar (__vsc_escape_value "$myVal") + if test (count $envVarsToReport) -gt 0 + __vsc_esc EnvSingleStart 1 + + for key in $envVarsToReport + if set -q $key + set -l value $$key + __vsc_esc EnvSingleEntry $key (__vsc_escape_value "$value") + end + end + + __vsc_esc EnvSingleEnd end - __vsc_esc EnvSingleEnd end end @@ -217,6 +226,11 @@ function __init_vscode_shell_integration end end +# Report prompt type +if set -q POSH_SESSION_ID + __vsc_esc P PromptType=oh-my-posh +end + # Report this shell supports rich command detection __vsc_esc P HasRichCommandDetection=True diff --git a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 4f4f5b722a5..c29140f2ea9 100644 --- a/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/code/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -27,6 +27,10 @@ $env:VSCODE_STABLE = $null $__vscode_shell_env_reporting = $env:VSCODE_SHELL_ENV_REPORTING $env:VSCODE_SHELL_ENV_REPORTING = $null +$Global:envVarsToReport = @() +if ($__vscode_shell_env_reporting) { + $Global:envVarsToReport = $__vscode_shell_env_reporting.Split(',') +} $osVersion = [System.Environment]::OSVersion.Version $isWindows10 = $IsWindows -and $osVersion.Major -eq 10 -and $osVersion.Minor -eq 0 -and $osVersion.Build -lt 22000 @@ -97,11 +101,15 @@ function Global:Prompt() { # Send current environment variables as JSON # OSC 633 ; EnvJson ; ; - if ($__vscode_shell_env_reporting -eq "1") { + if ($Global:envVarsToReport.Count -gt 0) { $envMap = @{} - Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } - $envJson = $envMap | ConvertTo-Json -Compress - $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" + foreach ($varName in $envVarsToReport) { + if (Test-Path "env:$varName") { + $envMap[$varName] = (Get-Item "env:$varName").Value + } + } + $envJson = $envMap | ConvertTo-Json -Compress + $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" } # Before running the original prompt, put $? back to what it was: @@ -124,10 +132,22 @@ function Global:Prompt() { return $Result } +# Report prompt type +if ($env:STARSHIP_SESSION_KEY) { + [Console]::Write("$([char]0x1b)]633;P;PromptType=starship`a") +} +elseif ($env:POSH_SESSION_ID) { + [Console]::Write("$([char]0x1b)]633;P;PromptType=oh-my-posh`a") +} +elseif ($Global:GitPromptSettings) { + [Console]::Write("$([char]0x1b)]633;P;PromptType=posh-git`a") +} + # Only send the command executed sequence when PSReadLine is loaded, if not shell integration should # still work thanks to the command line sequence if (Get-Module -Name PSReadLine) { [Console]::Write("$([char]0x1b)]633;P;HasRichCommandDetection=True`a") + $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine function Global:PSConsoleHostReadLine { $CommandLine = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 7e67399b2d6..fbf6c29bf3b 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -68,7 +68,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr element: editor, code: editor.getValue(), codeBlockIndex: 0, - languageId: editor.getModel()!.getLanguageId() + languageId: editor.getModel()!.getLanguageId(), + chatSessionId: this._terminalChatWidget.value.inlineChatWidget.chatWidget.viewModel?.sessionId }; } }, 'terminal')); diff --git a/code/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/code/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index ddf420a3539..19cea45b430 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -15,7 +15,7 @@ import { strictEqual } from 'assert'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; import { IChatAgent } from '../../../../chat/common/chatAgents.js'; import { importAMDNodeModule } from '../../../../../../amdX.js'; -import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { ChatAgentLocation, ChatMode } from '../../../../chat/common/constants.js'; suite('Terminal Initial Hint Addon', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -34,6 +34,7 @@ suite('Terminal Initial Hint Addon', () => { slashCommands: [{ name: 'test', description: 'test' }], disambiguation: [], locations: [ChatAgentLocation.fromRaw('terminal')], + modes: [ChatMode.Ask], invoke: async () => { return {}; } }; const editorAgent: IChatAgent = { @@ -45,6 +46,7 @@ suite('Terminal Initial Hint Addon', () => { metadata: {}, slashCommands: [{ name: 'test', description: 'test' }], locations: [ChatAgentLocation.fromRaw('editor')], + modes: [ChatMode.Ask], disambiguation: [], invoke: async () => { return {}; } }; diff --git a/code/src/vs/workbench/contrib/terminalContrib/history/common/history.ts b/code/src/vs/workbench/contrib/terminalContrib/history/common/history.ts index 29709a50e0c..8eaac217b45 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/history/common/history.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/history/common/history.ts @@ -299,7 +299,8 @@ export async function fetchZshHistory(accessor: ServicesAccessor): Promise = new Set(); for (let i = 0; i < fileLines.length; i++) { const sanitized = fileLines[i].replace(/\\\n/g, '\n').trim(); diff --git a/code/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts b/code/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts index 2fffe8c043b..c9db0ef431f 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts @@ -216,94 +216,117 @@ suite('Terminal history', () => { suite('fetchZshHistory', () => { let fileScheme: string; let filePath: string; - const fileContent: string = [ - ': 1655252330:0;single line command', - ': 1655252330:0;git commit -m "A wrapped line in pwsh history\\', - '\\', - 'Some commit description\\', - '\\', - 'Fixes #xyz"', - ': 1655252330:0;git status', - ': 1655252330:0;two "\\', - 'line"' - ].join('\n'); + const fileContentType = [ + { + type: 'simple', + content: [ + 'single line command', + 'git commit -m "A wrapped line in pwsh history\\', + '\\', + 'Some commit description\\', + '\\', + 'Fixes #xyz"', + 'git status', + 'two "\\', + 'line"' + ].join('\n') + }, + { + type: 'extended', + content: [ + ': 1655252330:0;single line command', + ': 1655252330:0;git commit -m "A wrapped line in pwsh history\\', + '\\', + 'Some commit description\\', + '\\', + 'Fixes #xyz"', + ': 1655252330:0;git status', + ': 1655252330:0;two "\\', + 'line"' + ].join('\n') + }, + ]; let instantiationService: TestInstantiationService; let remoteConnection: Pick | null = null; let remoteEnvironment: Pick | null = null; - setup(() => { - instantiationService = new TestInstantiationService(); - instantiationService.stub(IFileService, { - async readFile(resource: URI) { - const expected = URI.from({ scheme: fileScheme, path: filePath }); - strictEqual(resource.scheme, expected.scheme); - strictEqual(resource.path, expected.path); - return { value: VSBuffer.fromString(fileContent) }; - } - } as Pick); - instantiationService.stub(IRemoteAgentService, { - async getEnvironment() { return remoteEnvironment; }, - getConnection() { return remoteConnection; } - } as Pick); - }); - - teardown(() => { - instantiationService.dispose(); - }); - - if (!isWindows) { - suite('local', () => { - let originalEnvValues: { HOME: string | undefined }; + for (const { type, content } of fileContentType) { + suite(type, () => { setup(() => { - originalEnvValues = { HOME: env['HOME'] }; - env['HOME'] = '/home/user'; - remoteConnection = { remoteAuthority: 'some-remote' }; - fileScheme = Schemas.vscodeRemote; - filePath = '/home/user/.bash_history'; + instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, { + async readFile(resource: URI) { + const expected = URI.from({ scheme: fileScheme, path: filePath }); + strictEqual(resource.scheme, expected.scheme); + strictEqual(resource.path, expected.path); + return { value: VSBuffer.fromString(content) }; + } + } as Pick); + instantiationService.stub(IRemoteAgentService, { + async getEnvironment() { return remoteEnvironment; }, + getConnection() { return remoteConnection; } + } as Pick); }); + teardown(() => { - if (originalEnvValues['HOME'] === undefined) { - delete env['HOME']; - } else { - env['HOME'] = originalEnvValues['HOME']; - } + instantiationService.dispose(); }); - test('current OS', async () => { - filePath = '/home/user/.zsh_history'; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + + if (!isWindows) { + suite('local', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.bash_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('current OS', async () => { + filePath = '/home/user/.zsh_history'; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); + }); + } + suite('remote', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.zsh_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('Windows', async () => { + remoteEnvironment = { os: OperatingSystem.Windows }; + strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined); + }); + test('macOS', async () => { + remoteEnvironment = { os: OperatingSystem.Macintosh }; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); + test('Linux', async () => { + remoteEnvironment = { os: OperatingSystem.Linux }; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); }); }); } - suite('remote', () => { - let originalEnvValues: { HOME: string | undefined }; - setup(() => { - originalEnvValues = { HOME: env['HOME'] }; - env['HOME'] = '/home/user'; - remoteConnection = { remoteAuthority: 'some-remote' }; - fileScheme = Schemas.vscodeRemote; - filePath = '/home/user/.zsh_history'; - }); - teardown(() => { - if (originalEnvValues['HOME'] === undefined) { - delete env['HOME']; - } else { - env['HOME'] = originalEnvValues['HOME']; - } - }); - test('Windows', async () => { - remoteEnvironment = { os: OperatingSystem.Windows }; - strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined); - }); - test('macOS', async () => { - remoteEnvironment = { os: OperatingSystem.Macintosh }; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); - }); - test('Linux', async () => { - remoteEnvironment = { os: OperatingSystem.Linux }; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); - }); - }); }); suite('fetchPwshHistory', () => { let fileScheme: string; diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index e9014e9ad05..a862d2146db 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -33,6 +33,7 @@ import { ITerminalLogService } from '../../../../../platform/terminal/common/ter import { TerminalMultiLineLinkDetector } from './terminalMultiLineLinkDetector.js'; import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; import type { IHoverAction } from '../../../../../base/browser/ui/hover/hover.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => 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/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts index d93b62aa938..ea69ddc1154 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts @@ -314,6 +314,23 @@ function detectLinksViaSuffix(line: string): IParsedLink[] { prefix, suffix }); + + // If the path contains an opening bracket, provide the path starting immediately after + // the opening bracket as an additional result + const openingBracketMatch = path.matchAll(/(?[\[\(])(?![\]\)])/g); + for (const match of openingBracketMatch) { + const bracket = match.groups?.bracket; + if (bracket) { + results.push({ + path: { + index: linkStartIndex + (prefix?.text.length || 0) + match.index + 1, + text: path.substring(match.index + bracket.length) + }, + prefix, + suffix + }); + } + } } } diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index 9d4684a66fa..2109f3d52a3 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts @@ -322,6 +322,48 @@ suite('TerminalLinkParsing', () => { ); }); + test('should detect multiple links when opening brackets are in the text', () => { + deepStrictEqual( + detectLinks('notlink[foo:45]', OperatingSystem.Linux), + [ + { + path: { + index: 0, + text: 'notlink[foo' + }, + prefix: undefined, + suffix: { + col: undefined, + row: 45, + rowEnd: undefined, + colEnd: undefined, + suffix: { + index: 11, + text: ':45' + } + } + }, + { + path: { + index: 8, + text: 'foo' + }, + prefix: undefined, + suffix: { + col: undefined, + row: 45, + rowEnd: undefined, + colEnd: undefined, + suffix: { + index: 11, + text: ':45' + } + } + }, + ] as IParsedLink[] + ); + }); + test('should extract the link prefix', () => { deepStrictEqual( detectLinks('"foo", line 5, col 6', OperatingSystem.Linux), diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index a2275a18a59..04d0dfa7415 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -251,6 +251,13 @@ suite('Workbench - TerminalLocalLinkDetector', () => { { range: [[1, 1], [16, 1]], uri: URI.file('/parent/cwd/foo') } ]); }); + + test('should support finding links after brackets', async () => { + validResources = [URI.file('/parent/cwd/foo')]; + await assertLinks(TerminalBuiltinLinkType.LocalFile, 'bar[foo:5', [ + { range: [[5, 1], [9, 1]], uri: URI.file('/parent/cwd/foo') } + ]); + }); }); suite('macOS/Linux', () => { diff --git a/code/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts b/code/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts index e286592242d..4dfe8aec91b 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts +++ b/code/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/code/src/vs/workbench/contrib/terminalContrib/wslRecommendation/browser/terminal.wslRecommendation.contribution.ts b/code/src/vs/workbench/contrib/terminalContrib/wslRecommendation/browser/terminal.wslRecommendation.contribution.ts index 09aa79c7320..5ed1692b724 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/wslRecommendation/browser/terminal.wslRecommendation.contribution.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/wslRecommendation/browser/terminal.wslRecommendation.contribution.ts @@ -9,7 +9,7 @@ import { isWindows } from '../../../../../base/common/platform.js'; import { localize } from '../../../../../nls.js'; import { IExtensionManagementService } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js'; +import { INotificationService, NeverShowAgainScope, NotificationPriority, Severity } from '../../../../../platform/notification/common/notification.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { registerWorkbenchContribution2, WorkbenchPhase, type IWorkbenchContribution } from '../../../../common/contributions.js'; import { InstallRecommendedExtensionAction } from '../../../extensions/browser/extensionsActions.js'; @@ -67,6 +67,7 @@ export class TerminalWslRecommendationContribution extends Disposable implements ], { sticky: true, + priority: NotificationPriority.OPTIONAL, neverShowAgain: { id: 'terminalConfigHelper/launchRecommendationsIgnore', scope: NeverShowAgainScope.APPLICATION }, onCancel: () => { } } diff --git a/code/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/code/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 552bf1a2298..b781d5b157a 100644 --- a/code/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 6ba098b83f1..160800155fc 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -1246,10 +1246,33 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { }); } + private async _runByUris(accessor: ServicesAccessor, files: URI[]): Promise<{ completedAt: number | undefined }> { + const uriIdentity = accessor.get(IUriIdentityService); + const testService = accessor.get(ITestService); + const discovered: InternalTestItem[] = []; + for (const uri of files) { + for await (const file of testsInFile(testService, uriIdentity, uri, undefined, true)) { + discovered.push(file); + } + } + + if (discovered.length) { + const r = await testService.runTests({ tests: discovered, group: this.group }); + return { completedAt: r.completedAt }; + } + + return { completedAt: undefined }; + } + /** * @override */ - public run(accessor: ServicesAccessor) { + public run(accessor: ServicesAccessor, files?: URI[]) { + if (files?.length) { + return this._runByUris(accessor, files); + } + + const uriIdentity = accessor.get(IUriIdentityService); let editor = accessor.get(ICodeEditorService).getActiveCodeEditor(); if (!editor) { return; @@ -1264,7 +1287,6 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { } const testService = accessor.get(ITestService); - const demandedUri = model.uri.toString(); // Iterate through the entire collection and run any tests that are in the // uri. See #138007. @@ -1273,7 +1295,7 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { while (queue.length) { for (const id of queue.pop()!) { const node = testService.collection.getNodeById(id)!; - if (node.item.uri?.toString() === demandedUri) { + if (uriIdentity.extUri.isEqual(node.item.uri, model.uri)) { discovered.push(node); } else { queue.push(node.children); diff --git a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts index 4f06c5ee9ca..ce9d447356f 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index 4915553ce61..1895434ddba 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -49,6 +49,7 @@ import { TestingContextKeys } from '../../common/testingContextKeys.js'; import { cmpPriority } from '../../common/testingStates.js'; import { TestUriType, buildTestUri } from '../../common/testingUri.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { TestId } from '../../common/testId.js'; interface ITreeElement { @@ -79,9 +80,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 +92,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 +105,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 +121,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 +133,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 +185,29 @@ class TestCaseElement implements ITreeElement { public readonly results: ITestResult, public readonly test: TestResultItem, public readonly taskIndex: number, - ) { } + ) { + this.id = `${results.id}/${test.item.extId}`; + + const parentId = TestId.fromString(test.item.extId).parentId; + if (parentId) { + this.description = ''; + for (const part of parentId.idsToRoot()) { + if (part.isRoot) { break; } + const test = results.getStateById(part.toString()); + if (!test) { break; } + if (this.description.length) { + this.description += ' \u2039 '; + } + + this.description += test.item.label; + } + } + + this.context = { + $mid: MarshalledId.TestItemContext, + tests: [InternalTestItem.serialize(test)], + }; + } } class TaskElement implements ITreeElement { @@ -832,6 +860,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 +913,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/code/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/code/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 0894eeca52b..6e08a8843f4 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 421f9578df8..6083f5719c6 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -19,6 +19,7 @@ import { mapFindFirst } from '../../../../base/common/arraysFind.js'; import { RunOnceScheduler, disposableTimeout } from '../../../../base/common/async.js'; import { groupBy } from '../../../../base/common/collections.js'; import { Color, RGBA } from '../../../../base/common/color.js'; +import { compareFileNames } from '../../../../base/common/comparers.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { Iterable } from '../../../../base/common/iterator.js'; @@ -36,7 +37,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 +702,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 +778,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); @@ -1348,7 +1352,9 @@ class TreeSorter implements ITreeSorter { const sb = b.test.item.sortText; // If tests are in the same location and there's no preferred sortText, // keep the extension's insertion order (#163449). - return inSameLocation && !sa && !sb ? 0 : (sa || a.test.item.label).localeCompare(sb || b.test.item.label); + return inSameLocation && !sa && !sb + ? 0 + : compareFileNames(sa || a.test.item.label, sb || b.test.item.label); } } @@ -1545,6 +1551,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/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index bb40133b638..788ed44ccd0 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/common/testExclusions.ts b/code/src/vs/workbench/contrib/testing/common/testExclusions.ts index 5ac06c15fc1..408c8975b90 100644 --- a/code/src/vs/workbench/contrib/testing/common/testExclusions.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/code/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 1d865c3a37d..9c187e33e76 100644 --- a/code/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/common/testResultStorage.ts b/code/src/vs/workbench/contrib/testing/common/testResultStorage.ts index 2253712efb8..6b8ee0d1a63 100644 --- a/code/src/vs/workbench/contrib/testing/common/testResultStorage.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/common/testService.ts b/code/src/vs/workbench/contrib/testing/common/testService.ts index 4f9fbbd8e9a..643a04d2697 100644 --- a/code/src/vs/workbench/contrib/testing/common/testService.ts +++ b/code/src/vs/workbench/contrib/testing/common/testService.ts @@ -172,7 +172,7 @@ const waitForTestToBeIdle = (testService: ITestService, test: IncrementalTestCol * Iterator that expands to and iterates through tests in the file. Iterates * in strictly descending order. */ -export const testsInFile = async function* (testService: ITestService, ident: IUriIdentityService, uri: URI, waitForIdle = true): AsyncIterable { +export const testsInFile = async function* (testService: ITestService, ident: IUriIdentityService, uri: URI, waitForIdle = true, descendInFile = true): AsyncIterable { const queue = new LinkedList>(); const existing = [...testService.collection.getNodeByUrl(uri)]; @@ -194,6 +194,10 @@ export const testsInFile = async function* (testService: ITestService, ident: IU if (ident.extUri.isEqual(uri, test.item.uri)) { yield test; + + if (!descendInFile) { + continue; + } } if (ident.extUri.isEqualOrParent(uri, test.item.uri)) { diff --git a/code/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/code/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 00ea40df0f7..67febf8cb4a 100644 --- a/code/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index 6e0e6168111..fec6792ad2d 100644 --- a/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/code/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/code/src/vs/workbench/contrib/testing/test/common/testStubs.ts b/code/src/vs/workbench/contrib/testing/test/common/testStubs.ts index d484c5e4f62..f121bd9cd83 100644 --- a/code/src/vs/workbench/contrib/testing/test/common/testStubs.ts +++ b/code/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/code/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/code/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 64b0d0de73e..e64fcc3e374 100644 --- a/code/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/code/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -41,7 +41,7 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js'; import { defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; export const manageExtensionIcon = registerIcon('theme-selection-manage-extension', Codicon.gear, localize('manageExtensionIcon', 'Icon for the \'Manage\' action in the theme selection quick pick.')); @@ -53,7 +53,7 @@ enum ConfigureItem { CUSTOM_TOP_ENTRY = 'customTopEntry' } -class MarketplaceThemesPicker { +class MarketplaceThemesPicker implements IDisposable { private readonly _installedExtensions: Promise>; private readonly _marketplaceExtensions: Set = new Set(); private readonly _marketplaceThemes: ThemeItem[] = []; @@ -275,6 +275,7 @@ class MarketplaceThemesPicker { this._queryDelayer.dispose(); this._marketplaceExtensions.clear(); this._marketplaceThemes.length = 0; + this._onDidChange.dispose(); } } diff --git a/code/src/vs/workbench/contrib/update/browser/update.ts b/code/src/vs/workbench/contrib/update/browser/update.ts index 3def92a9336..a20d7aea012 100644 --- a/code/src/vs/workbench/contrib/update/browser/update.ts +++ b/code/src/vs/workbench/contrib/update/browser/update.ts @@ -13,7 +13,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IUpdateService, State as UpdateState, StateType, IUpdate, DisablementReason } from '../../../../platform/update/common/update.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js'; import { ReleaseNotesManager } from './releaseNotesEditor.js'; @@ -146,7 +146,8 @@ export class ProductContribution implements IWorkbenchContribution { const uri = URI.parse(releaseNotesUrl); openerService.open(uri); } - }] + }], + { priority: NotificationPriority.OPTIONAL } ); }); } @@ -319,7 +320,8 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu run: () => { this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); } - }] + }], + { priority: NotificationPriority.OPTIONAL } ); } @@ -355,7 +357,8 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu run: () => { this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); } - }] + }], + { priority: NotificationPriority.OPTIONAL } ); } @@ -388,7 +391,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu severity.Info, nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), actions, - { sticky: true } + { + sticky: true, + priority: NotificationPriority.OPTIONAL + } ); } diff --git a/code/src/vs/workbench/contrib/url/common/urlGlob.ts b/code/src/vs/workbench/contrib/url/common/urlGlob.ts index cc44a3dafc4..ff0456dcf03 100644 --- a/code/src/vs/workbench/contrib/url/common/urlGlob.ts +++ b/code/src/vs/workbench/contrib/url/common/urlGlob.ts @@ -5,89 +5,154 @@ import { URI } from '../../../../base/common/uri.js'; -// TODO: rewrite this to use URIs directly and validate each part individually -// instead of relying on memoization of the stringified URI. -export const testUrlMatchesGlob = (uri: URI, globUrl: string): boolean => { - let url = uri.with({ query: null, fragment: null }).toString(true); - const normalize = (url: string) => url.replace(/\/+$/, ''); - globUrl = normalize(globUrl); - url = normalize(url); - - const memo = Array.from({ length: url.length + 1 }).map(() => - Array.from({ length: globUrl.length + 1 }).map(() => undefined), - ); +/** + * Normalizes a URL by removing trailing slashes and query/fragment components. + * @param url The URL to normalize. + * @returns URI - The normalized URI object. + */ +function normalizeURL(url: string | URI): URI { + const uri = typeof url === 'string' ? URI.parse(url) : url; + return uri.with({ + // Remove trailing slashes + path: uri.path.replace(/\/+$/, ''), + // Remove query and fragment + query: null, + fragment: null, + }); +} - if (/^[^./:]*:\/\//.test(globUrl)) { - return doUrlMatch(memo, url, globUrl, 0, 0); - } +/** + * Checks if a given URL matches a glob URL pattern. + * The glob URL pattern can contain wildcards (*) and subdomain matching (*.) + * @param uri The URL to check. + * @param globUrl The glob URL pattern to match against. + * @returns boolean - True if the URL matches the glob URL pattern, false otherwise. + */ +export function testUrlMatchesGlob(uri: string | URI, globUrl: string): boolean { + const normalizedUrl = normalizeURL(uri); + let normalizedGlobUrl = normalizeURL(globUrl); - const scheme = /^(https?):\/\//.exec(url)?.[1]; - if (scheme) { - return doUrlMatch(memo, url, `${scheme}://${globUrl}`, 0, 0); + const globHasScheme = /^[^./:]*:\/\//.test(globUrl); + // if the glob does not have a scheme we assume the scheme is http or https + // so if the url doesn't have a scheme of http or https we return false + if (!globHasScheme) { + if (normalizedUrl.scheme !== 'http' && normalizedUrl.scheme !== 'https') { + return false; + } + normalizedGlobUrl = normalizeURL(`${normalizedUrl.scheme}://${globUrl}`); } - return false; -}; + return ( + doMemoUrlMatch(normalizedUrl.scheme, normalizedGlobUrl.scheme) && + // The authority is the only thing that should do port logic. + doMemoUrlMatch(normalizedUrl.authority, normalizedGlobUrl.authority, true) && + ( + // + normalizedGlobUrl.path === '/' || + doMemoUrlMatch(normalizedUrl.path, normalizedGlobUrl.path) + ) + ); +} + +/** + * @param normalizedUrlPart The normalized URL part to match. + * @param normalizedGlobUrlPart The normalized glob URL part to match against. + * @param includePortLogic Whether to include port logic in the matching process. + * @returns boolean - True if the URL part matches the glob URL part, false otherwise. + */ +function doMemoUrlMatch( + normalizedUrlPart: string, + normalizedGlobUrlPart: string, + includePortLogic: boolean = false, +) { + const memo = Array.from({ length: normalizedUrlPart.length + 1 }).map(() => + Array.from({ length: normalizedGlobUrlPart.length + 1 }).map(() => undefined), + ); -const doUrlMatch = ( + return doUrlPartMatch(memo, includePortLogic, normalizedUrlPart, normalizedGlobUrlPart, 0, 0); +} + +/** + * Recursively checks if a URL part matches a glob URL part. + * This function uses memoization to avoid recomputing results for the same inputs. + * It handles various cases such as exact matches, wildcard matches, and port logic. + * @param memo A memoization table to avoid recomputing results for the same inputs. + * @param includePortLogic Whether to include port logic in the matching process. + * @param urlPart The URL part to match with. + * @param globUrlPart The glob URL part to match against. + * @param urlOffset The current offset in the URL part. + * @param globUrlOffset The current offset in the glob URL part. + * @returns boolean - True if the URL part matches the glob URL part, false otherwise. + */ +function doUrlPartMatch( memo: (boolean | undefined)[][], - url: string, - globUrl: string, + includePortLogic: boolean, + urlPart: string, + globUrlPart: string, urlOffset: number, - globUrlOffset: number, -): boolean => { + globUrlOffset: number +): boolean { if (memo[urlOffset]?.[globUrlOffset] !== undefined) { return memo[urlOffset][globUrlOffset]!; } const options = []; - // Endgame. - // Fully exact match - if (urlOffset === url.length) { - return globUrlOffset === globUrl.length; + // We've reached the end of the url. + if (urlOffset === urlPart.length) { + // We're also at the end of the glob url as well so we have an exact match. + if (globUrlOffset === globUrlPart.length) { + return true; + } + + if (includePortLogic && globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === ':*') { + // any port match. Consume a port if it exists otherwise nothing. Always consume the base. + return globUrlOffset + 2 === globUrlPart.length; + } + + return false; } // Some path remaining in url - if (globUrlOffset === globUrl.length) { - const remaining = url.slice(urlOffset); + if (globUrlOffset === globUrlPart.length) { + const remaining = urlPart.slice(urlOffset); return remaining[0] === '/'; } - if (url[urlOffset] === globUrl[globUrlOffset]) { + if (urlPart[urlOffset] === globUrlPart[globUrlOffset]) { // Exact match. - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset + 1)); } - if (globUrl[globUrlOffset] + globUrl[globUrlOffset + 1] === '*.') { + if (globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === '*.') { // Any subdomain match. Either consume one thing that's not a / or : and don't advance base or consume nothing and do. - if (!['/', ':'].includes(url[urlOffset])) { - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset)); + if (!['/', ':'].includes(urlPart[urlOffset])) { + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset)); } - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 2)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 2)); } - if (globUrl[globUrlOffset] === '*') { + if (globUrlPart[globUrlOffset] === '*') { // Any match. Either consume one thing and don't advance base or consume nothing and do. - if (urlOffset + 1 === url.length) { + if (urlOffset + 1 === urlPart.length) { // If we're at the end of the input url consume one from both. - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset + 1)); } else { - options.push(doUrlMatch(memo, url, globUrl, urlOffset + 1, globUrlOffset)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset + 1, globUrlOffset)); } - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 1)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 1)); } - if (globUrl[globUrlOffset] + globUrl[globUrlOffset + 1] === ':*') { - // any port match. Consume a port if it exists otherwise nothing. Always comsume the base. - if (url[urlOffset] === ':') { + if (includePortLogic && globUrlPart[globUrlOffset] + globUrlPart[globUrlOffset + 1] === ':*') { + // any port match. Consume a port if it exists otherwise nothing. Always consume the base. + if (urlPart[urlOffset] === ':') { let endPortIndex = urlOffset + 1; - do { endPortIndex++; } while (/[0-9]/.test(url[endPortIndex])); - options.push(doUrlMatch(memo, url, globUrl, endPortIndex, globUrlOffset + 2)); + do { endPortIndex++; } while (/[0-9]/.test(urlPart[endPortIndex])); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, endPortIndex, globUrlOffset + 2)); } else { - options.push(doUrlMatch(memo, url, globUrl, urlOffset, globUrlOffset + 2)); + options.push(doUrlPartMatch(memo, includePortLogic, urlPart, globUrlPart, urlOffset, globUrlOffset + 2)); } } return (memo[urlOffset][globUrlOffset] = options.some(a => a === true)); -}; +} diff --git a/code/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/code/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index 50b2af6a09d..390d9897097 100644 --- a/code/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/code/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -113,4 +113,9 @@ suite('Link protection domain matching', () => { linkAllowedByRules('https://github.com/login/oauth/authorize?foo=4', ['https://github.com/login/oauth/authorize']); linkAllowedByRules('https://github.com/login/oauth/authorize#foo', ['https://github.com/login/oauth/authorize']); }); + + test('ensure individual parts of url are compared and wildcard does not leak out', () => { + linkNotAllowedByRules('https://x.org/github.com', ['https://*.github.com']); + linkNotAllowedByRules('https://x.org/y.github.com', ['https://*.github.com']); + }); }); diff --git a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index 04a69674350..0e66b1e031e 100644 --- a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -962,11 +962,11 @@ class ProfileNameRenderer extends ProfilePropertyRenderer { } } )); - nameInput.onDidChange(value => { + disposables.add(nameInput.onDidChange(value => { if (profileElement && value) { profileElement.root.name = value; } - }); + })); const focusTracker = disposables.add(trackFocus(nameInput.inputElement)); disposables.add(focusTracker.onDidBlur(() => { if (profileElement && !nameInput.value) { @@ -1500,7 +1500,7 @@ class ContentsProfileRenderer extends ProfilePropertyRenderer { })); }, disposables, - elementDisposables: new DisposableStore() + elementDisposables }; } @@ -1681,7 +1681,7 @@ class ProfileWorkspacesRenderer extends ProfilePropertyRenderer { })); }, disposables, - elementDisposables: new DisposableStore() + elementDisposables }; } @@ -2113,16 +2113,23 @@ interface IActionsColumnTemplateData { readonly disposables: DisposableStore; } -class ChangeProfileAction extends Action { +class ChangeProfileAction implements IAction { + + readonly id = 'changeProfile'; + readonly label = 'Change Profile'; + readonly class = ThemeIcon.asClassName(editIcon); + readonly enabled = true; + readonly tooltip = localize('change profile', "Change Profile"); + readonly checked = false; constructor( private readonly item: WorkspaceTableElement, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ) { - super('changeProfile', '', ThemeIcon.asClassName(editIcon)); - this.tooltip = localize('change profile', "Change Profile"); } + run(): void { } + getSwitchProfileActions(): IAction[] { return this.userDataProfilesService.profiles .filter(profile => !profile.isTransient) diff --git a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 5ef8a898c17..9a937a500f9 100644 --- a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -527,7 +527,7 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement { const extensions = await this.extensionManagementService.getInstalled(undefined, this.profile.extensionsResource); const extension = extensions.find(e => areSameExtensions(e.identifier, child.identifier)); if (extension) { - await this.extensionManagementService.toggleAppliationScope(extension, this.profile.extensionsResource); + await this.extensionManagementService.toggleApplicationScope(extension, this.profile.extensionsResource); } } }] diff --git a/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 1273b8c7a9a..84b5eacefa3 100644 --- a/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/code/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -419,6 +419,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD // The extensionId and purpose in the URL are used for filtering in js-debug: const params: { [key: string]: string } = { id: this.id, + parentId: targetWindow.vscodeWindowId.toString(), origin: this.origin, swVersion: String(this._expectedServiceWorkerVersion), extensionId: extension?.id.value ?? '', diff --git a/code/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css b/code/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css deleted file mode 100644 index 79dade268d9..00000000000 --- a/code/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -.monaco-dialog-box { - border-radius: 6px; -} - -.dialog-message-detail-title > div > p > .codicon[class*='codicon-']::before{ - position: relative; - color: var(--vscode-textLink-foreground); - padding-right: 10px; - font-size: larger; -} - -.dialog-message-detail-title { - height: 22px; - font-size: large; -} - -.monaco-dialog-box .monaco-action-bar .actions-container { - justify-content: flex-end; -} diff --git a/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts b/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts deleted file mode 100644 index ece6304fe9c..00000000000 --- a/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts +++ /dev/null @@ -1,111 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from '../../../common/contributions.js'; -import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; -import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { WelcomeWidget } from './welcomeWidget.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { localize } from '../../../../nls.js'; -import { applicationConfigurationNodeBase } from '../../../common/configuration.js'; -import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; - -const configurationKey = 'workbench.welcome.experimental.dialog'; - -class WelcomeDialogContribution extends Disposable implements IWorkbenchContribution { - - private isRendered = false; - - constructor( - @IStorageService storageService: IStorageService, - @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, - @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService contextService: IContextKeyService, - @ICodeEditorService codeEditorService: ICodeEditorService, - @IInstantiationService instantiationService: IInstantiationService, - @ICommandService commandService: ICommandService, - @ITelemetryService telemetryService: ITelemetryService, - @IOpenerService openerService: IOpenerService, - @IEditorService editorService: IEditorService - ) { - super(); - - if (!storageService.isNew(StorageScope.APPLICATION)) { - return; // do not show if this is not the first session - } - - const setting = configurationService.inspect(configurationKey); - if (!setting.value) { - return; - } - - const welcomeDialog = environmentService.options?.welcomeDialog; - if (!welcomeDialog) { - return; - } - - this._register(editorService.onDidActiveEditorChange(() => { - if (!this.isRendered) { - - const codeEditor = codeEditorService.getActiveCodeEditor(); - if (codeEditor?.hasModel()) { - const scheduler = new RunOnceScheduler(() => { - const notificationsVisible = contextService.contextMatchesRules(ContextKeyExpr.deserialize('notificationCenterVisible')) || - contextService.contextMatchesRules(ContextKeyExpr.deserialize('notificationToastsVisible')); - if (codeEditor === codeEditorService.getActiveCodeEditor() && !notificationsVisible) { - this.isRendered = true; - - const welcomeWidget = new WelcomeWidget( - codeEditor, - instantiationService, - commandService, - telemetryService, - openerService); - - welcomeWidget.render(welcomeDialog.title, - welcomeDialog.message, - welcomeDialog.buttonText, - welcomeDialog.buttonCommand); - } - }, 3000); - - this._register(codeEditor.onDidChangeModelContent((e) => { - if (!this.isRendered) { - scheduler.schedule(); - } - })); - } - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(WelcomeDialogContribution, LifecyclePhase.Eventually); - -const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -configurationRegistry.registerConfiguration({ - ...applicationConfigurationNodeBase, - properties: { - 'workbench.welcome.experimental.dialog': { - scope: ConfigurationScope.APPLICATION, - type: 'boolean', - default: false, - tags: ['experimental'], - description: localize('workbench.welcome.dialog', "When enabled, a welcome widget is shown in the editor") - } - } -}); diff --git a/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts b/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts deleted file mode 100644 index c33e006b35e..00000000000 --- a/code/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts +++ /dev/null @@ -1,218 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/welcomeWidget.css'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; -import { $, append, hide } from '../../../../base/browser/dom.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ButtonBar } from '../../../../base/browser/ui/button/button.js'; -import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; -import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { localize } from '../../../../nls.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { LinkedText, parseLinkedText } from '../../../../base/common/linkedText.js'; -import { Link } from '../../../../platform/opener/browser/link.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; -import { Color } from '../../../../base/common/color.js'; -import { contrastBorder, editorWidgetBackground, editorWidgetForeground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; - -export class WelcomeWidget extends Disposable implements IOverlayWidget { - - private readonly _rootDomNode: HTMLElement; - private readonly element: HTMLElement; - private readonly messageContainer: HTMLElement; - private readonly markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - - constructor( - private readonly _editor: ICodeEditor, - private readonly instantiationService: IInstantiationService, - private readonly commandService: ICommandService, - private readonly telemetryService: ITelemetryService, - private readonly openerService: IOpenerService - ) { - super(); - this._rootDomNode = document.createElement('div'); - this._rootDomNode.className = 'welcome-widget'; - - this.element = this._rootDomNode.appendChild($('.monaco-dialog-box')); - this.element.setAttribute('role', 'dialog'); - - hide(this._rootDomNode); - - this.messageContainer = this.element.appendChild($('.dialog-message-container')); - } - - async executeCommand(commandId: string, ...args: string[]) { - try { - await this.commandService.executeCommand(commandId, ...args); - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: commandId, - from: 'welcomeWidget' - }); - } - catch (ex) { - } - } - - public async render(title: string, message: string, buttonText: string, buttonAction: string) { - if (!this._editor._getViewModel()) { - return; - } - - await this.buildWidgetContent(title, message, buttonText, buttonAction); - this._editor.addOverlayWidget(this); - this._show(); - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'welcomeWidgetRendered', - from: 'welcomeWidget' - }); - } - - private async buildWidgetContent(title: string, message: string, buttonText: string, buttonAction: string) { - - const actionBar = this._register(new ActionBar(this.element, {})); - - const action = this._register(new Action('dialog.close', localize('dialogClose', "Close Dialog"), ThemeIcon.asClassName(Codicon.dialogClose), true, async () => { - this._hide(); - })); - actionBar.push(action, { icon: true, label: false }); - - const renderBody = (message: string, icon: string): MarkdownString => { - const mds = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true }); - mds.appendMarkdown(`
    $(${icon})`); - mds.appendMarkdown(message); - return mds; - }; - - const titleElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail-title')); - const titleElementMdt = this.markdownRenderer.render(renderBody(title, 'zap')); - titleElement.appendChild(titleElementMdt.element); - - this.buildStepMarkdownDescription(this.messageContainer, message.split('\n').filter(x => x).map(text => parseLinkedText(text))); - - const buttonsRowElement = this.messageContainer.appendChild($('.dialog-buttons-row')); - const buttonContainer = buttonsRowElement.appendChild($('.dialog-buttons')); - - const buttonBar = this._register(new ButtonBar(buttonContainer)); - const primaryButton = this._register(buttonBar.addButtonWithDescription({ title: true, secondary: false, ...defaultButtonStyles })); - primaryButton.label = mnemonicButtonLabel(buttonText, true); - - this._register(primaryButton.onDidClick(async () => { - await this.executeCommand(buttonAction); - })); - - buttonBar.buttons[0].focus(); - } - - private buildStepMarkdownDescription(container: HTMLElement, text: LinkedText[]) { - for (const linkedText of text) { - const p = append(container, $('p')); - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - const labelWithIcon = renderLabelWithIcons(node); - for (const element of labelWithIcon) { - if (typeof element === 'string') { - p.appendChild(renderFormattedText(element, { inline: true, renderCodeSegments: true })); - } else { - p.appendChild(element); - } - } - } else { - const link = this.instantiationService.createInstance(Link, p, node, { - opener: (href: string) => { - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'welcomeWidetLinkAction', - from: 'welcomeWidget' - }); - this.openerService.open(href, { allowCommands: true }); - } - }); - this._register(link); - } - } - } - return container; - } - - getId(): string { - return 'editor.contrib.welcomeWidget'; - } - - getDomNode(): HTMLElement { - return this._rootDomNode; - } - - getPosition(): IOverlayWidgetPosition | null { - return { - preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER - }; - } - - private _isVisible: boolean = false; - - private _show(): void { - if (this._isVisible) { - return; - } - this._isVisible = true; - this._rootDomNode.style.display = 'block'; - } - - private _hide(): void { - if (!this._isVisible) { - return; - } - - this._isVisible = true; - this._rootDomNode.style.display = 'none'; - this._editor.removeOverlayWidget(this); - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'welcomeWidgetDismissed', - from: 'welcomeWidget' - }); - } -} - -registerThemingParticipant((theme, collector) => { - const addBackgroundColorRule = (selector: string, color: Color | undefined): void => { - if (color) { - collector.addRule(`.monaco-editor ${selector} { background-color: ${color}; }`); - } - }; - - const widgetBackground = theme.getColor(editorWidgetBackground); - addBackgroundColorRule('.welcome-widget', widgetBackground); - - const widgetShadowColor = theme.getColor(widgetShadow); - if (widgetShadowColor) { - collector.addRule(`.welcome-widget { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`); - } - - const widgetBorderColor = theme.getColor(widgetBorder); - if (widgetBorderColor) { - collector.addRule(`.welcome-widget { border-left: 1px solid ${widgetBorderColor}; border-right: 1px solid ${widgetBorderColor}; border-bottom: 1px solid ${widgetBorderColor}; }`); - } - - const hcBorder = theme.getColor(contrastBorder); - if (hcBorder) { - collector.addRule(`.welcome-widget { border: 1px solid ${hcBorder}; }`); - } - - const foreground = theme.getColor(editorWidgetForeground); - if (foreground) { - collector.addRule(`.welcome-widget { color: ${foreground}; }`); - } -}); diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index a57f9c15dd7..6aa706f7bf5 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -80,7 +80,7 @@ registerAction2(class extends Action2 { if (!selectedCategory && !selectedStep) { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, - options: { preserveFocus: toSide ?? false, inactive } + options: { preserveFocus: toSide ?? false, inactive, forceReload: true } }, toSide ? SIDE_GROUP : undefined); return; } @@ -90,6 +90,10 @@ registerAction2(class extends Action2 { if (group.activeEditor instanceof GettingStartedInput) { const activeEditor = group.activeEditor as GettingStartedInput; activeEditor.showWelcome = false; + if (activeEditor.selectedCategory && activeEditor.selectedStep) { + // currently in a walkthrough. + return; + } (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); return; } @@ -236,6 +240,11 @@ registerAction2(class extends Action2 { title: localize2('welcome.showAllWalkthroughs', 'Open Walkthrough...'), category, f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 3, + }, }); } @@ -278,14 +287,43 @@ registerAction2(class extends Action2 { })); disposables.add(quickPick.onDidHide(() => disposables.dispose())); await extensionService.whenInstalledExtensionsRegistered(); - gettingStartedService.onDidAddWalkthrough(async () => { + disposables.add(gettingStartedService.onDidAddWalkthrough(async () => { quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); - }); + })); quickPick.show(); quickPick.busy = false; } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'welcome.showNewWelcome', + title: localize2('welcome.showNewWelcome', 'Open New Welcome Experience'), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const options: GettingStartedEditorOptions = { selectedCategory: 'NewWelcomeExperience', forceReload: true, showTelemetryNotice: true }; + + editorService.openEditor({ + resource: GettingStartedInput.RESOURCE, + options + }); + } +}); + +CommandsRegistry.registerCommand({ + id: 'welcome.newWorkspaceChat', + handler: (accessor, stepID: string) => { + const commandService = accessor.get(ICommandService); + commandService.executeCommand('workbench.action.chat.open', { mode: 'agent', query: '#new ', isPartialQuery: true }); + } +}); + export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | 'webworker' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote or serverless contexts may be different from the platform of the UI")); class WorkspacePlatformContribution { diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 50d59c25ab3..a13221c132b 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -60,10 +60,10 @@ import { IWebviewElement, IWebviewService } from '../../webview/browser/webview. import './gettingStartedColors.js'; import { GettingStartedDetailsRenderer } from './gettingStartedDetailsRenderer.js'; import { gettingStartedCheckedCodicon, gettingStartedUncheckedCodicon } from './gettingStartedIcons.js'; -import { GettingStartedInput } from './gettingStartedInput.js'; +import { GettingStartedEditorOptions, GettingStartedInput } from './gettingStartedInput.js'; import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey, parseDescription } from './gettingStartedService.js'; import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from './startupPage.js'; -import { startEntries } from '../common/gettingStartedContent.js'; +import { NEW_WELCOME_EXPERIENCE, startEntries } from '../common/gettingStartedContent.js'; import { GroupDirection, GroupsOrder, IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -253,7 +253,8 @@ export class GettingStartedPage extends EditorPane { })); this._register(this.gettingStartedService.onDidProgressStep(step => { - const category = this.gettingStartedCategories.find(category => category.id === step.category); + const category = step.category === NEW_WELCOME_EXPERIENCE ? this.gettingStartedService.getWalkthrough(step.category) : + this.gettingStartedCategories.find(c => c.id === step.category); if (!category) { throw Error('Could not find category with ID: ' + step.category); } const ourStep = category.steps.find(_step => _step.id === step.id); if (!ourStep) { @@ -343,6 +344,7 @@ export class GettingStartedPage extends EditorPane { override async setInput(newInput: GettingStartedInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { this.container.classList.remove('animatable'); this.editorInput = newInput; + this.editorInput.showTelemetryNotice = (options as GettingStartedEditorOptions)?.showTelemetryNotice ?? true; await super.setInput(newInput, options, context, token); await this.buildCategoriesSlide(); if (this.shouldAnimate()) { @@ -919,19 +921,28 @@ export class GettingStartedPage extends EditorPane { this.registerDispatchListeners(); if (this.editorInput.selectedCategory) { + const showNewExperience = this.editorInput.selectedCategory === NEW_WELCOME_EXPERIENCE; this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (!this.currentWalkthrough) { this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); - this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); + this.currentWalkthrough = showNewExperience ? this.gettingStartedService.getWalkthrough(this.editorInput.selectedCategory) : this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (this.currentWalkthrough) { - this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + if (showNewExperience) { + this.buildNewCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } else { + this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } this.setSlide('details'); return; } } else { - this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + if (showNewExperience) { + this.buildNewCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } else { + this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } this.setSlide('details'); return; } @@ -1181,6 +1192,7 @@ export class GettingStartedPage extends EditorPane { this.container.classList.toggle('height-constrained', size.height <= 600); this.container.classList.toggle('width-constrained', size.width <= 400); this.container.classList.toggle('width-semi-constrained', size.width <= 950); + this.container.classList.toggle('new-layout-width-constrained', size.width <= 800); this.categoriesPageScrollbar?.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); @@ -1190,7 +1202,8 @@ export class GettingStartedPage extends EditorPane { private updateCategoryProgress() { this.window.document.querySelectorAll('.category-progress').forEach(element => { const categoryID = element.getAttribute('x-data-category-id'); - const category = this.gettingStartedCategories.find(category => category.id === categoryID); + const category = categoryID === NEW_WELCOME_EXPERIENCE ? this.gettingStartedService.getWalkthrough(categoryID) : + this.gettingStartedCategories.find(c => c.id === categoryID); if (!category) { throw Error('Could not find category with ID ' + categoryID); } const stats = this.getWalkthroughCompletionStats(category); @@ -1219,7 +1232,8 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); } - const ourCategory = this.gettingStartedCategories.find(c => c.id === categoryID); + const ourCategory = categoryID === NEW_WELCOME_EXPERIENCE ? this.gettingStartedService.getWalkthrough(categoryID) : + this.gettingStartedCategories.find(c => c.id === categoryID); if (!ourCategory) { throw Error('Could not find category with ID: ' + categoryID); } @@ -1356,7 +1370,7 @@ export class GettingStartedPage extends EditorPane { if (isCommand) { const keybinding = this.getKeyBinding(command); - if (keybinding) { + if (keybinding && this.editorInput.selectedCategory !== NEW_WELCOME_EXPERIENCE) { const shortcutMessage = $('span.shortcut-message', {}, localize('gettingStarted.keyboardTip', 'Tip: Use keyboard shortcut ')); container.appendChild(shortcutMessage); const label = new KeybindingLabel(shortcutMessage, OS, { ...defaultKeybindingLabelStyles }); @@ -1394,7 +1408,444 @@ export class GettingStartedPage extends EditorPane { super.clearInput(); } + + private selectStepByIndex(newIndex: number, steps: IResolvedWalkthroughStep[], direction: number) { + const currentIndex = steps.findIndex(step => step.id === this.editorInput.selectedStep); + + // Update the selected step and build its media + this.selectSlide(steps[newIndex].id); + + // update footer visibility + const footer = this.stepsContent.querySelector('.getting-started-footer') as HTMLElement; + if (footer && newIndex !== 0) { + footer.style.display = 'none'; + } else if (footer) { + footer.style.display = 'block'; + } + + this.updateNavButtons(newIndex, steps); + + // Update the active dot + const dots = this.stepsContent.querySelectorAll('.step-dot'); + dots.forEach((dot, index) => { + if (index === newIndex) { + dot.classList.add('active'); + } else { + dot.classList.remove('active'); + } + }); + + + if (currentIndex === newIndex) { + return; // No change + } + + const slidesContainer = this.stepsContent.querySelector('.step-slides-container') as HTMLElement; + if (slidesContainer) { + // Apply the transform to move the slides + const slides = slidesContainer.querySelectorAll('.step-slide'); + + // First make all slides visible for the animation + slides.forEach((slide, index) => { + const slideElement = slide as HTMLElement; + // Position all slides in their starting positions + if (index === currentIndex) { + slideElement.style.display = 'block'; + slideElement.style.transform = 'translateX(0)'; + } else if (index === newIndex) { + slideElement.style.display = 'block'; + slideElement.style.transform = `translateX(${direction < 0 ? '-100%' : '100%'})`; + } else { + slideElement.style.display = 'none'; + } + }); + + // Force a reflow to ensure the initial positions are applied + slidesContainer.getBoundingClientRect(); + + // Now animate to the final positions + setTimeout(() => { + slides.forEach((slide, index) => { + const slideElement = slide as HTMLElement; + if (index === currentIndex) { + slideElement.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`; + setTimeout(() => { + slideElement.style.display = 'none'; + }, SLIDE_TRANSITION_TIME_MS); + } else if (index === newIndex) { + slideElement.style.transform = 'translateX(0)'; + } + }); + }, 20); + } + } + + private updateNavButtons(newIndex: number, steps: IResolvedWalkthroughStep[]) { + const prevButton = this.stepsContent.querySelector('.button-link.navigation.back') as HTMLButtonElement; + if (newIndex === 0) { + if (prevButton) { + prevButton.classList.add('inactive'); + prevButton.setAttribute('aria-hidden', 'true'); + prevButton.setAttribute('tabindex', '-1'); + } + } + else { + if (prevButton) { + prevButton.classList.remove('inactive'); + prevButton.removeAttribute('aria-hidden'); + prevButton.removeAttribute('tabindex'); + } + } + } + + private buildNewCategorySlide(categoryID: string, selectedStep?: string) { + this.container.classList.add('newSlide'); + if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } + + this.detailsPageDisposables.clear(); + this.mediaDisposables.clear(); + + const category = this.gettingStartedService.getWalkthrough(categoryID); + if (!category) { + throw Error('could not find category with ID ' + categoryID); + } + + // Filter steps based on when context + const steps = category.steps.filter(step => this.contextService.contextMatchesRules(step.when)); + + const groupedSteps = new Map(); + steps.forEach(step => { + const prefixMatch = step.id.match(/^([^.]+)\./); + const prefix = prefixMatch ? prefixMatch[1] : step.id; + if (!groupedSteps.has(prefix)) { + groupedSteps.set(prefix, []); + } + groupedSteps.get(prefix)?.push(step); + }); + + // Create the slide container that will hold all step slides + const slidesContainer = $('.step-slides-container'); + + const navigationContainer = $('.step-dots-container'); + + // Add back button + const prevButton = $('button.button-link.navigation.back', { + 'aria-label': localize('previousStep', "Previous Step"), + 'tabindex': '0' + }, $('span.codicon.codicon-arrow-left'), localize('back', "Back")); + + const dotsContainer = $('.dots-centered'); + navigationContainer.appendChild(prevButton); + navigationContainer.appendChild(dotsContainer); + + const allSlides: { id: string; steps: IResolvedWalkthroughStep[] }[] = []; + groupedSteps.forEach((stepsInGroup, prefix) => { + if (stepsInGroup.length === 1) { + allSlides.push({ id: stepsInGroup[0].id, steps: [stepsInGroup[0]] }); + } else { + // For multi-steps, group them into a single slide + allSlides.push({ id: prefix, steps: stepsInGroup }); + } + }); + + allSlides.forEach((slide, index) => { + // Create the slide element + const slideElement = $('.step-slide', { 'data-step': slide.id }); + + // Create the content container with flex layout + const slideContent = $('.step-slide-content'); + + // Text content column + const textContent = $('.step-text-content'); + + if (slide.steps.length === 1) { + // Single step case + const step = slide.steps[0]; + + // Create step title + const titleElement = $('h3.step-title', { 'x-step-title-for': step.id }); + reset(titleElement, ...renderLabelWithIcons(step.title)); + textContent.appendChild(titleElement); + + // Create step description container + const descriptionContainer = $('.step-description', { 'x-step-description-for': step.id }); + this.buildMarkdownDescription(descriptionContainer, step.description); + textContent.appendChild(descriptionContainer); + } else { + // Multi-step case - group steps with same prefix into a single slide + const multiStepContainer = $('.multi-step-container'); + + slide.steps.forEach((step, i) => { + const subStep = $('.sub-step', { 'data-sub-step-id': step.id }); + + this.detailsPageDisposables.add(addDisposableListener(subStep, 'click', () => { + this.selectSubStep(step.id); + })); + this.detailsPageDisposables.add(addDisposableListener(subStep, 'mouseenter', () => { + this.selectSubStep(step.id); + })); + + const subStepTitleEl = $('.sub-step-title', {}, ...renderLabelWithIcons(step.title)); + subStep.appendChild(subStepTitleEl); + + const subStepDesc = $('.sub-step-description'); + this.buildMarkdownDescription(subStepDesc, [step.description[0]]); + subStep.appendChild(subStepDesc); + + if (i === 0 || step.id === this.editorInput.selectedStep) { + subStep.classList.add('active'); + } else { + subStep.classList.remove('active'); + } + + multiStepContainer.appendChild(subStep); + }); + + // Get the linkedText of the lastStep + const lastStep = slide.steps[slide.steps.length - 1]; + const linkedText = lastStep.description.length > 1 ? lastStep.description[1] : undefined; + if (linkedText) { + const descElement = $('.multi-step-action'); + this.buildMarkdownDescription(descElement, [linkedText]); + multiStepContainer.appendChild(descElement); + } + + textContent.appendChild(multiStepContainer); + } + + // Append text content to the slide + slideContent.appendChild(textContent); + slideElement.appendChild(slideContent); + slidesContainer.appendChild(slideElement); + + // Create dot for this slide + const dot = $('button.step-dot', { + 'data-step-dot-index': `${index}`, + 'role': 'button' + }); + + // Set the initial active dot + if (index === 0) { + dot.classList.add('active'); + } + + dotsContainer.appendChild(dot); + + this.detailsPageDisposables.add(addDisposableListener(dot, 'click', () => { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex === index) { + return; + } + this.selectStepByIndex(index, allSlides.map(s => s.steps[0]), index > currentIndex ? 1 : -1); + })); + }); + + // Add next button + const nextButton = $('button.button-link.navigation.next', { + 'aria-label': localize('nextStep', "Next"), + }, localize('next', "Next"), $('span.codicon.codicon-arrow-right')); + + navigationContainer.appendChild(nextButton); + this.detailsPageDisposables.add(addDisposableListener(prevButton, 'click', () => { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex > 0) { + this.selectStepByIndex(currentIndex - 1, allSlides.map(s => s.steps[0]), -1); + } + })); + + this.detailsPageDisposables.add(addDisposableListener(nextButton, 'click', () => { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex < allSlides.length - 1) { + this.selectStepByIndex(currentIndex + 1, allSlides.map(s => s.steps[0]), 1); + } else { + this.scrollPrev(); + } + })); + + // Set the current walkthrough and step + this.currentWalkthrough = category; + this.editorInput.selectedCategory = categoryID; + this.editorInput.selectedStep = this.currentWalkthrough.steps[0].id; + + // Category title and description + const categoryHeader = $('.category-header'); + const categoryTitle = $('h2.category-title', { 'x-category-title-for': category.id }); + reset(categoryTitle, ...renderLabelWithIcons(category.title)); + categoryHeader.appendChild(categoryTitle); + + const descriptionContainer = $('.category-description.description.max-lines-3', { 'x-category-description-for': category.id }); + this.buildMarkdownDescription(descriptionContainer, parseDescription(category.description)); + reset(descriptionContainer, ...renderLabelWithIcons(category.description)); + categoryHeader.appendChild(descriptionContainer); + + const categoryFooter = $('.getting-started-footer'); + if (this.editorInput.showTelemetryNotice && getTelemetryLevel(this.configurationService) !== TelemetryLevel.NONE && this.productService.enableTelemetry) { + this.buildTelemetryFooter(categoryFooter); + } + + // Build the container for the whole slide deck + const stepsContainer = $('.getting-started-steps-container', {}, + categoryHeader, + slidesContainer, + navigationContainer, + categoryFooter, + ); + + // Set up the scroll container + this.detailsScrollbar = this._register(new DomScrollableElement(stepsContainer, { className: 'steps-container' })); + const stepListComponent = this.detailsScrollbar.getDomNode(); + + // Append to the content area + reset(this.stepsContent, stepListComponent); + stepListComponent.tabIndex = 0; + stepListComponent.focus(); + + this.selectStepByIndex(0, this.currentWalkthrough.steps, 1); + + // Add keyboard navigation + this.detailsPageDisposables.add(addDisposableListener(stepListComponent, 'keydown', (e) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.RightArrow) { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex < allSlides.length - 1) { + this.selectStepByIndex(currentIndex + 1, allSlides.map(s => s.steps[0]), 1); + } + else { + this.scrollPrev(); + } + } else if (event.keyCode === KeyCode.LeftArrow) { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex > 0) { + this.selectStepByIndex(currentIndex - 1, allSlides.map(s => s.steps[0]), -1); + } + } else if (event.keyCode === KeyCode.UpArrow || event.keyCode === KeyCode.DownArrow) { + const currentIndex = this.getCurrentSlideIndex(allSlides); + if (currentIndex > 0) { + return; + } + this.navigateWithinMultiStepContainer(event.keyCode); + } + })); + + // Register listeners for step selection + this.registerDispatchListeners(); + + this.detailsScrollbar.scanDomNode(); + this.detailsPageScrollbar?.scanDomNode(); + } + + private navigateWithinMultiStepContainer(keyCode: KeyCode) { + const currentElement = this.container.querySelector(`.multi-step-container`) as HTMLElement; + if (!currentElement) { return; } + const currentSubStep = currentElement.querySelector('.sub-step.active'); + const allElements = Array.from(this.container.querySelectorAll('.sub-step')); + const currentIndex = currentSubStep ? allElements.indexOf(currentSubStep as HTMLElement) : -1; + + let targetElement: HTMLElement | undefined; + if (keyCode === KeyCode.UpArrow && currentIndex > 0) { + targetElement = allElements[currentIndex - 1] as HTMLElement; + } else if (keyCode === KeyCode.DownArrow && currentIndex < allElements.length - 1) { + targetElement = allElements[currentIndex + 1] as HTMLElement; + } + + if (targetElement) { + const stepId = targetElement.getAttribute('data-sub-step-id'); + this.selectSubStep(stepId!); + targetElement.focus(); + } + } + + private selectSubStep(selectedStepId: string) { + if (this.editorInput.selectedStep === selectedStepId) { + return; + } + this.editorInput.selectedStep = selectedStepId; + + const multiStepContainer = this.container.querySelector('.multi-step-container'); + if (!multiStepContainer) { return; } + + const subSteps = multiStepContainer.querySelectorAll('.sub-step'); + subSteps.forEach(subStepEl => { + const stepId = subStepEl.getAttribute('data-sub-step-id'); + if (stepId === selectedStepId) { + subStepEl.classList.add('active'); + } else { + subStepEl.classList.remove('active'); + } + }); + + const prefixMatch = selectedStepId.match(/^([^.]+)\./); + const prefix = prefixMatch ? prefixMatch[1] : selectedStepId; + this.selectSlideWithPrefix(selectedStepId, prefix); + + this.gettingStartedService.progressByEvent('stepSelected:' + selectedStepId); + } + + private selectSlideWithPrefix(stepId: string, prefix: string) { + this.editorInput.selectedStep = stepId; + + const step = this.currentWalkthrough?.steps.find(step => step.id === stepId); + if (!step) { return; } + + const selectedSlide = this.stepsContent.querySelector(`.step-slide[data-step="${prefix}"]`); + if (selectedSlide) { + const selectedSlideContent = selectedSlide.querySelector('.step-slide-content'); + this.mediaDisposables.clear(); + this.stepDisposables.clear(); + this.buildMediaComponent(this.editorInput.selectedStep); + selectedSlideContent?.appendChild(this.stepMediaComponent); + setTimeout(() => (selectedSlideContent as HTMLElement).focus(), 0); + } + + this.gettingStartedService.progressByEvent('stepSelected:' + stepId); + this.detailsPageScrollbar?.scanDomNode(); + this.detailsScrollbar?.scanDomNode(); + } + + private getCurrentSlideIndex(allSlides: { id: string; steps: IResolvedWalkthroughStep[] }[]): number { + if (!this.editorInput.selectedStep) { + return 0; + } + + // Check if the selected step is directly a slide ID + const directMatch = allSlides.findIndex(slide => slide.id === this.editorInput.selectedStep); + if (directMatch !== -1) { + return directMatch; + } + + // Otherwise, find which slide contains the step as a sub-step + return allSlides.findIndex(slide => + slide.steps.some(step => step.id === this.editorInput.selectedStep) + ); + } + + private selectSlide(stepId: string) { + this.editorInput.selectedStep = stepId; + + const step = this.currentWalkthrough?.steps.find(step => step.id === stepId); + if (!step) { return; } + + + const effectiveStepId = stepId.match(/^([^.]+)\./)?.[1] ?? stepId; + const selectedSlide = this.stepsContent.querySelector(`.step-slide[data-step="${effectiveStepId}"]`); + + if (selectedSlide) { + const selectedSlideContent = selectedSlide.querySelector('.step-slide-content'); + this.mediaDisposables.clear(); + this.stepDisposables.clear(); + this.buildMediaComponent(this.editorInput.selectedStep); + selectedSlideContent?.appendChild(this.stepMediaComponent); + setTimeout(() => (selectedSlideContent as HTMLElement).focus(), 0); + } + + this.gettingStartedService.progressByEvent('stepSelected:' + stepId); + this.detailsPageScrollbar?.scanDomNode(); + this.detailsScrollbar?.scanDomNode(); + } + private buildCategorySlide(categoryID: string, selectedStep?: string) { + this.container.classList.remove('newSlide'); + if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } this.extensionService.whenInstalledExtensionsRegistered().then(() => { @@ -1405,7 +1856,8 @@ export class GettingStartedPage extends EditorPane { this.detailsPageDisposables.clear(); this.mediaDisposables.clear(); - const category = this.gettingStartedCategories.find(category => category.id === categoryID); + const category = categoryID === NEW_WELCOME_EXPERIENCE ? this.gettingStartedService.getWalkthrough(categoryID) : + this.gettingStartedCategories.find(category => category.id === categoryID); if (!category) { throw Error('could not find category with ID ' + categoryID); } @@ -1557,7 +2009,8 @@ export class GettingStartedPage extends EditorPane { const text = localize({ key: 'footer', comment: ['fist substitution is "vs code", second is "privacy statement", third is "opt out".'] }, "{0} collects usage data. Read our {1} and learn how to {2}.", this.productService.nameShort, privacyStatementButton, optOutButton); - parent.append(mdRenderer.render({ value: text, isTrusted: true }).element); + const renderedContents = this.detailsPageDisposables.add(mdRenderer.render({ value: text, isTrusted: true })); + parent.append(renderedContents.element); } private getKeybindingLabel(command: string) { @@ -1627,8 +2080,12 @@ export class GettingStartedPage extends EditorPane { const prevButton = this.container.querySelector('.prev-button.button-link'); prevButton!.style.display = this.editorInput.showWelcome || this.prevWalkthrough ? 'block' : 'none'; - const moreTextElement = prevButton!.querySelector('.moreText'); - moreTextElement!.textContent = firstLaunch ? localize('welcome', "Welcome") : localize('goBack', "Go Back"); + if (this.editorInput.selectedCategory === NEW_WELCOME_EXPERIENCE) { + prevButton!.style.display = 'none'; + } else { + const moreTextElement = prevButton!.querySelector('.moreText'); + moreTextElement!.textContent = firstLaunch ? localize('welcome', "Welcome") : localize('goBack', "Go Back"); + } this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = true); diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExpService.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExpService.ts new file mode 100644 index 00000000000..75f3c86b733 --- /dev/null +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExpService.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; + +export interface IGettingStartedExperiment { + cohort: number; + experimentGroup: string; +} + +export const IGettingStartedExperimentService = createDecorator('gettingStartedExperimentService'); + +export interface IGettingStartedExperimentService { + readonly _serviceBrand: undefined; + getCurrentExperiment(): IGettingStartedExperiment; +} + +const EXPERIMENT_STORAGE_KEY = 'gettingStartedExperiment'; + +interface ExperimentGroupDefinition { + name: string; + min: number; + max: number; +} + +export enum GettingStartedExperimentGroup { + New = 'newExp', + Default = 'defaultExp' +} + +const STABLE_EXPERIMENT_GROUPS: ExperimentGroupDefinition[] = [ + // { name: GettingStartedExperimentGroup.New, min: 0.0, max: 0.1 }, + //{ name: GettingStartedExperimentGroup.Default, min: 0.1, max: 1.0 } + { name: GettingStartedExperimentGroup.Default, min: 0.0, max: 1.0 } +]; + +const INSIDERS_EXPERIMENT_GROUPS: ExperimentGroupDefinition[] = [ + // { name: GettingStartedExperimentGroup.New, min: 0.0, max: 0.3 }, + //{ name: GettingStartedExperimentGroup.Default, min: 0.3, max: 1.0 } + { name: GettingStartedExperimentGroup.Default, min: 0.0, max: 1.0 } +]; + +const DEFAULT_EXPERIMENT_GROUPS: ExperimentGroupDefinition[] = [ + { name: GettingStartedExperimentGroup.Default, min: 0.0, max: 1.0 } +]; + +export class GettingStartedExperimentService extends Disposable implements IGettingStartedExperimentService { + declare readonly _serviceBrand: undefined; + + private readonly experiment: IGettingStartedExperiment; + + constructor( + @IStorageService private readonly storageService: IStorageService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService private readonly productService: IProductService, + ) { + super(); + this.experiment = this.getOrCreateExperiment(); + this.sendExperimentTelemetry(); + } + + private getExperimentAllocation(): ExperimentGroupDefinition[] { + const quality = this.productService.quality; + if (quality === 'stable') { + return STABLE_EXPERIMENT_GROUPS; + } else if (quality === 'insider') { + return INSIDERS_EXPERIMENT_GROUPS; + } else { + return DEFAULT_EXPERIMENT_GROUPS; + } + } + + private getOrCreateExperiment(): IGettingStartedExperiment { + const storedExperiment = this.storageService.get(EXPERIMENT_STORAGE_KEY, StorageScope.APPLICATION); + if (storedExperiment) { + try { + return JSON.parse(storedExperiment); + } catch (e) { + this.storageService.remove(EXPERIMENT_STORAGE_KEY, StorageScope.APPLICATION); + } + } + + const newExperiment = this.createNewExperiment(); + + this.storageService.store( + EXPERIMENT_STORAGE_KEY, + JSON.stringify(newExperiment), + StorageScope.APPLICATION, + StorageTarget.MACHINE + ); + + return newExperiment; + } + + private createNewExperiment(): IGettingStartedExperiment { + const cohort = Math.random(); + const experimentGroups = this.getExperimentAllocation(); + + let experimentGroup = GettingStartedExperimentGroup.Default; + for (const group of experimentGroups) { + if (cohort >= group.min && cohort < group.max) { + experimentGroup = group.name as GettingStartedExperimentGroup; + break; + } + } + + return { cohort, experimentGroup }; + } + + private sendExperimentTelemetry(): void { + type GettingStartedExperimentClassification = { + owner: 'bhavyaus'; + comment: 'Records which experiment cohort the user is in for getting started experience'; + cohort: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The exact cohort number for the user' }; + experimentGroup: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The experiment group the user is in' }; + }; + + type GettingStartedExperimentEvent = { + cohort: number; + experimentGroup: string; + }; + + this.telemetryService.publicLog2( + 'gettingStarted.experimentCohort', + { + cohort: this.experiment.cohort, + experimentGroup: this.experiment.experimentGroup + } + ); + } + + getCurrentExperiment(): IGettingStartedExperiment { + return this.experiment; + } +} + +registerSingleton(IGettingStartedExperimentService, GettingStartedExperimentService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts index 7375366e5c6..cb40a3ddafa 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts @@ -19,6 +19,7 @@ export interface GettingStartedEditorOptions extends IEditorOptions { showTelemetryNotice?: boolean; showWelcome?: boolean; walkthroughPageTitle?: string; + showNewExperience?: boolean; } export class GettingStartedInput extends EditorInput { @@ -29,6 +30,7 @@ export class GettingStartedInput extends EditorInput { private _selectedStep: string | undefined; private _showTelemetryNotice: boolean; private _showWelcome: boolean; + private _walkthroughPageTitle: string | undefined; override get typeId(): string { diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 76113eb4c08..630a26f614c 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -500,6 +500,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }; }) .filter(category => category.content.type !== 'steps' || category.content.steps.length) + .filter(category => category.id !== 'NewWelcomeExperience') .map(category => this.resolveWalkthrough(category)); return categoriesWithCompletion; diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index cd312dfbe33..ad6d0335a67 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -10,7 +10,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer { box-sizing: border-box; - line-height: 22px; + line-height: 16px; position: relative; overflow: hidden; height: inherit; @@ -134,7 +134,8 @@ grid-template-areas: "left-column" "right-column" "footer"; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header, .monaco-workbench .part.editor > .content .gettingStartedContainer.height-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header { +.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header, +.monaco-workbench .part.editor > .content .gettingStartedContainer.height-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header { display: none; } @@ -142,7 +143,8 @@ display: none; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry, .gettingStartedContainer.noExtensions { +.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry, +.gettingStartedContainer.noExtensions { display: unset; } @@ -270,7 +272,7 @@ font-size: 13px; box-sizing: border-box; line-height: normal; - margin: 8px 8px 8px 0; + margin: 8px 8px 8px 1px; padding: 3px 6px 6px; text-align: left; } @@ -299,7 +301,7 @@ font-size: 16px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content:not(:empty){ +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content:not(:empty) { margin-bottom: 8px; } @@ -360,6 +362,7 @@ position: relative; top: auto; } + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-category img.category-icon { margin-right: 10px; margin-left: 10px; @@ -571,6 +574,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .steps-container { height: 100%; + align-self: center; grid-area: steps; } @@ -583,7 +587,7 @@ grid-area: steps-start / media-start / footer-start / media-end; align-self: self-start; display: flex; - justify-content:center ; + justify-content: center; height: 100%; width: 100%; } @@ -655,7 +659,7 @@ display: inline; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .index-list.getting-started { +.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .index-list.getting-started { display: none; } @@ -690,7 +694,12 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer button:focus-visible { outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; + outline-offset: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer .step-list-container button:focus-visible { + box-shadow: inset 0 0 0 1px var(--vscode-focusBorder); + outline: none; } .monaco-workbench .part.editor > .content .gettingStartedContainer .prev-button.button-link { @@ -810,7 +819,8 @@ background: transparent; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .openAWalkthrough > button, .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .showOnStartup { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .openAWalkthrough > button, +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .showOnStartup { text-align: center; display: flex; justify-content: center; @@ -829,7 +839,7 @@ } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox.codicon:not(.checked)::before { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox.codicon:not(.checked)::before { opacity: 0; } @@ -867,7 +877,8 @@ line-height: 1.3em; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-step .step-description-container .monaco-button, .monaco-workbench .part.editor > .content .gettingStartedContainer .max-lines-3 { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-step .step-description-container .monaco-button, +.monaco-workbench .part.editor > .content .gettingStartedContainer .max-lines-3 { /* Supported everywhere: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp#browser_compatibility */ -webkit-line-clamp: 3; display: -webkit-box; @@ -905,6 +916,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .description { color: var(--vscode-descriptionForeground); line-height: 1.4em; + font-size: 1.4em; } .monaco-workbench .part.editor > .content .gettingStartedContainer .category-progress .message { @@ -936,7 +948,7 @@ outline-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); } -.monaco-workbench .part.editor > .content .gettingStartedContainer button.expanded:hover { +.monaco-workbench .part.editor > .content .gettingStartedContainer button.expanded:hover { background: var(--vscode-welcomePage-tileBackground); } @@ -974,7 +986,7 @@ color: var(--vscode-textLink-activeForeground); } -.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.hide-category-button):active { +.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.hide-category-button):active { color: var(--vscode-textLink-activeForeground); } @@ -994,7 +1006,7 @@ border: 1px solid var(--vscode-contrastBorder); } -.monaco-workbench .part.editor > .content .gettingStartedContainer button.button-link { +.monaco-workbench .part.editor > .content .gettingStartedContainer button.button-link { border: inherit; } @@ -1029,3 +1041,447 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { border-color: var(--vscode-checkbox-border) !important; } + +/* Full width layout for the new slide design */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent { + height: 100%; + max-width: 100%; + margin: 0 auto; + padding: 0 32px; + display: flex; +} + +/* Back button position */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .prev-button { + padding: 16px 32px 0; + position: static; + margin: 0; +} + +/* Title area */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-category, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .category-header { + grid-area: header; + text-align: left; + align-self: center; + display: flex; + flex-direction: column; + flex-wrap: wrap; + overflow: visible; + white-space: wrap; +} + +/* Steps container - takes most of the space */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .steps-container { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + align-self: center; + outline: none; +} + +/* Hide the default media container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media { + display: none; +} + +/* Getting Started Steps Container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .getting-started-steps-container { + max-width: 1000px; + max-height: 800px; + margin: 0 auto; + display: grid; + padding-left: 10%; + padding-right: 10%; + grid-template-rows: 25% 50% 5% auto; + grid-template-areas: + "header" + "slides" + "dots" + "footer"; + height: 100%; + width: 100%; + align-self: center; + /* Center vertically in parent */ +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.width-semi-constrained .getting-started-steps-container, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.width-semi-constrained .getting-started-steps-container { + padding-left: 8px; + padding-right: 8px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails h2 { + font-size: 40px; +} + +/* Step slides container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slides-container { + grid-area: slides; + margin: 0; + overflow: hidden; + flex: 1; + width: 100%; + transition: transform 0.25s, opacity 0.25s; + padding-top: 16px; + padding-bottom: 16px; +} + +/* Individual slide styling */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide { + min-width: 100%; + height: 100%; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + transition: transform 0.25s, opacity 0.25s; +} + +/* Two-column layout for slide content */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide-content { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: "text media"; + max-width: 1200px; + width: 100%; + height: 100%; +} + +/* Left column - text content only */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content { + grid-area: text; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 300px; + height: 100%; + flex-wrap: wrap; + overflow: visible; + white-space: wrap; +} + +/* Right column - for media content */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + grid-area: media; + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + height: 100%; + width: 80%; +} + +/* Navigation buttons in dots container for newSlide scenario */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container { + width: 75%; + max-width: 900px; + margin: 12px auto; + display: flex; + justify-content: center; + align-items: center; + grid-area: dots; + height: max-content; + position: relative; +} + +/* Center dots between navigation buttons */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .dots-centered { + display: flex; + gap: 32px; + justify-content: center; + align-items: center; + width: max-content; + flex: 1; + margin: 0 8px; + position: absolute; +} + +/* Navigation buttons styling */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.navigation { + display: flex; + align-items: center; + cursor: pointer; + padding: 3px 12px; + font-size: 16px; + line-height: 1.5; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; + position: absolute; +} + +/* Make the back/next button icons larger */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.navigation .codicon { + font-size: 18px; + padding-left: 4px; + padding-right: 4px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.navigation.inactive { + display: none; +} + +/* Remove auto margins that spread things out */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.back { + margin: 0; + position: absolute; + left: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.next { + margin: 0; + position: absolute; + right: 0; +} + +/* Left alignment for back button */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.back { + margin-right: auto; +} + +/* Right alignment for next button */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .button-link.next { + margin-left: auto; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot { + width: 12px; + /* Increased from 13px */ + height: 12px; + /* Increased from 13px */ + background-color: var(--vscode-button-secondaryBackground); + border: none; + border-radius: 50%; + cursor: pointer; + padding: 0; + transition: transform 0.25s ease, background-color 0.25s ease; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot:hover { + transform: scale(1.2); + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot.active { + background-color: var(--vscode-button-background); + width: 16px; + height: 16px; +} + +/* Footer area */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-footer, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .getting-started-footer { + grid-area: footer; + align-self: flex-end; + justify-self: center; + text-align: center; + flex-direction: column; + align-items: center; +} + +/* Step title styling */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide h3.step-title { + font-size: 2.25em; + /* Increased from 1.5em */ + margin: 0 0 24px 0; + /* Increased from 16px bottom margin */ + padding: 0; + line-height: 1.5; +} + +/* Increase description text size */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-description { + font-size: 16px; + /* Increased from default */ + line-height: 1.5; +} + +/* Buttons in description */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-description .button-container .monaco-button { + height: 36px; + padding: 0 16px; + font-size: 15px; +} + +/* Multi-step container buttons */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .multi-step-container .button-container .monaco-button { + height: 36px; + padding: 0 16px; + font-size: 16px; +} + +/* Responsive design - stack on smaller screens */ +@media (max-width: 600px) { + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide-content { + grid-template-columns: 1fr; + grid-template-areas: + "text"; + gap: 24px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content { + padding-right: 2px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + display: none; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide { + padding: 0 16px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content, + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + min-height: unset; + height: auto; + } +} + +/* Animation for slide transitions */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.animatable .step-slides-container { + transition: transform 0.25s, opacity 0.25s; +} + +/* Low motion preference */ +.monaco-workbench.reduce-motion .part.editor > .content .gettingStartedContainer.newSlide .step-slides-container { + transition: none; +} + +/* Hide moreText and prev-button in newSlide scenarios */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .moreText, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .prev-button, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .prev-button.button-link, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .prev-button { + display: none; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-description .button-container { + margin-top: 50px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-description .button-container .monaco-button { + height: 40px; + width: fit-content; + display: flex; + padding: 0 10%; + align-items: center; + font-size: 16px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .multi-step-container { + display: flex; + flex-direction: column; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .sub-step { + display: flex; + flex-direction: column; + cursor: pointer; + box-sizing: border-box; + border-left: 1px solid transparent; + padding-left: 8px; + border-width: 1px; + border-style: solid; + border-color: transparent; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .sub-step-title { + font-size: 20px; + line-height: 24px; + margin: 0; + padding-top: 8px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .sub-step.active { + background: var(--vscode-welcomePage-tileHoverBackground); + border-color: var(--vscode-welcomePage-tileBorder); + border-radius: 6px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .multi-step-container .button-container { + margin-top: 2rem; + margin-left: 16px +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.width-constrained .multi-step-container .button-container, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.width-semi-constrained .multi-step-container .button-container { + margin-top: 1rem; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.width-constrained .sub-step-title { + padding-top: 2px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .step-slide-content, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.new-layout-width-constrained .step-slide-content { + grid-template-columns: 1fr; + grid-template-areas: "text"; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .step-text-content, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.new-layout-width-constrained .step-text-content { + width: 100%; + padding-right: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .getting-started-media, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.new-layout-width-constrained .getting-started-media { + display: none; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .step-media-content, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.new-layout-width-constrained .step-media-content { + display: none; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .getting-started-steps-container, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.new-layout-width-constrained .getting-started-steps-container { + width: 90%; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .category-header { + display: none; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .getting-started-steps-container { + grid-template-rows: 5% 70% 5% auto; + grid-template-areas: + "." + "slides" + "dots" + "footer"; + padding-top: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .step-slides-container { + padding-top: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained h3.step-title { + margin-bottom: 12px; + font-size: 1.8em; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.height-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent > .steps-container { + padding-top: 0; + margin-top: 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .multi-step-container .button-container .monaco-button { + height: 40px; + width: fit-content; + display: flex; + padding: 0 10%; + align-items: center; + min-width: max-content; + font-size: 16px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStarted.showDetails .gettingStartedSlideCategories { + left: 100%; + opacity: 0; +} diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index 9f54e5df21e..e728e1941f8 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -14,7 +14,7 @@ import { IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE, WorkbenchStat import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IWorkingCopyBackupService } from '../../../services/workingCopy/common/workingCopyBackup.js'; import { ILifecycleService, LifecyclePhase, StartupKind } from '../../../services/lifecycle/common/lifecycle.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { joinPath } from '../../../../base/common/resources.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; @@ -37,7 +37,7 @@ const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; const telemetryOptOutStorageKey = 'workbench.telemetryOptOutShown'; -export class StartupPageEditorResolverContribution implements IWorkbenchContribution { +export class StartupPageEditorResolverContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.startupPageEditorResolver'; @@ -45,6 +45,10 @@ export class StartupPageEditorResolverContribution implements IWorkbenchContribu @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorResolverService editorResolverService: IEditorResolverService ) { + super(); + const disposables = new DisposableStore(); + this._register(disposables); + editorResolverService.registerEditor( `${GettingStartedInput.RESOURCE.scheme}:/**`, { @@ -59,7 +63,7 @@ export class StartupPageEditorResolverContribution implements IWorkbenchContribu { createEditorInput: ({ resource, options }) => { return { - editor: this.instantiationService.createInstance(GettingStartedInput, options as GettingStartedEditorOptions), + editor: disposables.add(this.instantiationService.createInstance(GettingStartedInput, options as GettingStartedEditorOptions)), options: { ...options, pinned: false @@ -132,7 +136,7 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe if (startupEditorSetting.value === 'readme') { await this.openReadme(); } else if (startupEditorSetting.value === 'welcomePage' || startupEditorSetting.value === 'welcomePageInEmptyWorkbench') { - await this.openGettingStarted(); + await this.openGettingStarted(true); } else if (startupEditorSetting.value === 'terminal') { this.commandService.executeCommand(TerminalCommandId.CreateTerminalEditor); } diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index c7074a5f81e..949cea754cc 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import themePickerContent from './media/theme_picker.js'; +import themePickerSmallContent from './media/theme_picker_small.js'; import notebookProfileContent from './media/notebookProfile.js'; import { localize } from '../../../../nls.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -51,12 +52,14 @@ export async function moduleToContent(resource: URI): Promise { } gettingStartedContentRegistry.registerProvider('vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker', themePickerContent); +gettingStartedContentRegistry.registerProvider('vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker_small', themePickerSmallContent); gettingStartedContentRegistry.registerProvider('vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile', notebookProfileContent); // Register empty media for accessibility walkthrough gettingStartedContentRegistry.registerProvider('vs/workbench/contrib/welcomeGettingStarted/common/media/empty', () => ''); const setupIcon = registerIcon('getting-started-setup', Codicon.zap, localize('getting-started-setup-icon', "Icon used for the setup category of welcome page")); const beginnerIcon = registerIcon('getting-started-beginner', Codicon.lightbulb, localize('getting-started-beginner-icon', "Icon used for the beginner category of welcome page")); +export const NEW_WELCOME_EXPERIENCE = 'NewWelcomeExperience'; export type BuiltinGettingStartedStep = { id: string; @@ -174,17 +177,6 @@ export const startEntries: GettingStartedStartEntryContent = [ command: 'command:remoteHub.openRepository', } }, - { - id: 'topLevelShowWalkthroughs', - title: localize('gettingStarted.topLevelShowWalkthroughs.title', "Open a Walkthrough..."), - description: localize('gettingStarted.topLevelShowWalkthroughs.description', "View a walkthrough on the editor or an extension"), - icon: Codicon.checklist, - when: 'allWalkthroughsHidden', - content: { - type: 'startEntry', - command: 'command:welcome.showAllWalkthroughs', - } - }, { id: 'topLevelRemoteOpen', title: localize('gettingStarted.topLevelRemoteOpen.title', "Connect to..."), @@ -207,6 +199,17 @@ export const startEntries: GettingStartedStartEntryContent = [ command: 'command:workbench.action.remote.showWebStartEntryActions', } }, + { + id: 'topLevelNewWorkspaceChat', + title: localize('gettingStarted.newWorkspaceChat.title', "New Workspace with Copilot..."), + description: localize('gettingStarted.newWorkspaceChat.description', "Create a new workspace with Copilot"), + icon: Codicon.copilot, + when: '!isWeb && !chatSetupHidden', + content: { + type: 'startEntry', + command: 'command:welcome.newWorkspaceChat', + } + }, ]; const Button = (title: string, href: string) => `[${title}](${href})`; @@ -226,7 +229,7 @@ function createCopilotSetupStep(id: string, button: string, when: string, includ id, title: CopilotStepTitle, description, - when, + when: `${when} && !chatSetupHidden`, media: { type: 'svg', altText: 'VS Code Copilot multi file edits', path: 'multi-file-edits.svg' }, @@ -236,7 +239,7 @@ function createCopilotSetupStep(id: string, button: string, when: string, includ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'Setup', - title: localize('gettingStarted.setup.title', "Get Started with VS Code"), + title: localize('gettingStarted.setup.title', "Get started with VS Code"), description: localize('gettingStarted.setup.description', "Customize your editor, learn the basics, and start coding"), isFeatured: true, icon: setupIcon, @@ -247,7 +250,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ type: 'steps', steps: [ createCopilotSetupStep('CopilotSetupSignedOut', CopilotSignedOutButton, 'chatSetupSignedOut', true), - createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanLimited)', false), + createCopilotSetupStep('CopilotSetupComplete', CopilotCompleteButton, 'chatSetupInstalled && (chatPlanPro || chatPlanProPlus || chatPlanBusiness || chatPlanEnterprise || chatPlanLimited)', false), createCopilotSetupStep('CopilotSetupSignedIn', CopilotSignedInButton, '!chatSetupSignedOut && (!chatSetupInstalled || chatPlanCanSignUp)', true), { id: 'pickColorTheme', @@ -664,5 +667,78 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, ] } + }, + { + id: `${NEW_WELCOME_EXPERIENCE}`, + title: localize('gettingStarted.new.title', "Get started with VS Code"), + description: localize('gettingStarted.new.description', "Supercharge coding with AI"), + isFeatured: false, + icon: setupIcon, + when: '!isWeb', + walkthroughPageTitle: localize('gettingStarted.new.walkthroughPageTitle', 'Set up VS Code'), + content: { + type: 'steps', + steps: [ + { + id: 'copilotSetup.chat', + title: localize('gettingStarted.agentMode.title', "Agent mode"), + description: localize('gettingStarted.agentMode.description', "Analyzes the problem, plans next steps, and makes changes for you."), + media: { + type: 'svg', altText: 'VS Code Copilot multi file edits', path: 'multi-file-edits.svg' + }, + }, + { + id: 'copilotSetup.inline', + title: localize('gettingStarted.nes.title', "Next Edit Suggestions"), + description: localize('gettingStarted.nes.description', "Get code suggestions that predict your next edit."), + media: { + type: 'svg', altText: 'Next Edit Suggestions', path: 'ai-powered-suggestions.svg' + }, + }, + { + id: 'copilotSetup.customize', + title: localize('gettingStarted.customize.title', "Personalized to how you work"), + description: localize('gettingStarted.customize.description', "Swap models, add agent mode tools, and create personalized instructions.\n{0}", Button(localize('signUp', "Set up AI"), 'command:workbench.action.chat.triggerSetup')), + media: { + type: 'svg', altText: 'Personalize', path: 'multi-file-edits.svg' + }, + }, + { + id: 'newCommandPaletteTask', + title: localize('newgettingStarted.commandPalette.title', "All commands within reach"), + description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), + media: { type: 'svg', altText: 'Command Palette overlay for searching and executing commands.', path: 'commandPalette.svg' }, + }, + { + id: 'newPickColorTheme', + title: localize('gettingStarted.pickColor.title', "Choose your theme"), + description: localize('gettingStarted.pickColor.description.interpolated', "The right theme helps you focus on your code, is easy on your eyes, and is simply more fun to use.\n{0}", Button(localize('titleID', "Browse Color Themes"), 'command:workbench.action.selectTheme')), + completionEvents: [ + 'onSettingChanged:workbench.colorTheme', + 'onCommand:workbench.action.selectTheme' + ], + media: { type: 'markdown', path: 'theme_picker_small', } + }, + { + id: 'newFindLanguageExtensions', + title: localize('newgettingStarted.findLanguageExts.title', "Support for all languages"), + description: localize('newgettingStarted.findLanguageExts.description.interpolated', "Install the language extensions you need in your toolkit.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), + when: 'workspacePlatform != \'webworker\'', + media: { + type: 'svg', altText: 'Language extensions', path: 'languages.svg' + }, + }, + { + id: 'newSettingsAndSync', + title: localize('newgettingStarted.settings.title', "Customize every aspect of VS Code"), + description: localize('newgettingStarted.settingsAndSync.description.interpolated', "[Back up and sync](command:workbench.userDataSync.actions.turnOn) settings across all your devices.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), + when: 'syncStatus != uninitialized', + completionEvents: ['onEvent:sync-enabled'], + media: { + type: 'svg', altText: 'VS Code Settings', path: 'settings.svg' + }, + }, + ] + } } ]; diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg new file mode 100644 index 00000000000..c7e582b4d15 --- /dev/null +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/ai-powered-suggestions.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg index 9106074c4c0..8dee86eda63 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg @@ -1,513 +1,377 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker_small.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker_small.ts new file mode 100644 index 00000000000..8f59ab26a41 --- /dev/null +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker_small.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { escape } from '../../../../../base/common/strings.js'; +import { localize } from '../../../../../nls.js'; +import { ThemeSettingDefaults } from '../../../../services/themes/common/workbenchThemeService.js'; + +export default () => ` + +
    + + + ${escape(localize('dark', "Dark Modern"))} + + + + ${escape(localize('light', "Light Modern"))} + +
    +
    + + + ${escape(localize('HighContrast', "Dark High Contrast"))} + + + + ${escape(localize('HighContrastLight', "Light High Contrast"))} + +
    + +`; diff --git a/code/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts b/code/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts index 55371fbeea8..57fc1eb5de3 100644 --- a/code/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts +++ b/code/src/vs/workbench/contrib/workspaces/browser/workspaces.contribution.ts @@ -77,7 +77,7 @@ export class WorkspacesFinderContribution extends Disposable implements IWorkben run: () => this.hostService.openWindow([{ workspaceUri: joinPath(folder, workspaceFile) }]) }], { neverShowAgain, - priority: !this.storageService.isNew(StorageScope.WORKSPACE) ? NotificationPriority.SILENT : undefined // https://github.com/microsoft/vscode/issues/125315 + priority: !this.storageService.isNew(StorageScope.WORKSPACE) ? NotificationPriority.SILENT : NotificationPriority.OPTIONAL // https://github.com/microsoft/vscode/issues/125315 }); } @@ -99,7 +99,7 @@ export class WorkspacesFinderContribution extends Disposable implements IWorkben } }], { neverShowAgain, - priority: !this.storageService.isNew(StorageScope.WORKSPACE) ? NotificationPriority.SILENT : undefined // https://github.com/microsoft/vscode/issues/125315 + priority: !this.storageService.isNew(StorageScope.WORKSPACE) ? NotificationPriority.SILENT : NotificationPriority.OPTIONAL // https://github.com/microsoft/vscode/issues/125315 }); } } diff --git a/code/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/code/src/vs/workbench/electron-sandbox/actions/windowActions.ts index ce2a584233d..424db028d1c 100644 --- a/code/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/code/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -28,6 +28,9 @@ import { KeybindingWeight } from '../../../platform/keybinding/common/keybinding import { isMacintosh } from '../../../base/common/platform.js'; import { getActiveWindow } from '../../../base/browser/dom.js'; import { IOpenedAuxiliaryWindow, IOpenedMainWindow, isOpenedAuxiliaryWindow } from '../../../platform/window/common/window.js'; +import { IsAuxiliaryTitleBarContext, IsAuxiliaryWindowFocusedContext, IsWindowAlwaysOnTopContext } from '../../common/contextkeys.js'; +import { isAuxiliaryWindow } from '../../../base/browser/window.js'; +import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js'; export class CloseWindowAction extends Action2 { @@ -418,3 +421,87 @@ export const ToggleWindowTabsBarHandler: ICommandHandler = function (accessor: S return accessor.get(INativeHostService).toggleWindowTabsBar(); }; + +export class ToggleWindowAlwaysOnTopAction extends Action2 { + + static readonly ID = 'workbench.action.toggleWindowAlwaysOnTop'; + + constructor() { + super({ + id: ToggleWindowAlwaysOnTopAction.ID, + title: localize2('toggleWindowAlwaysOnTop', "Toggle Window Always on Top"), + f1: true, + precondition: IsAuxiliaryWindowFocusedContext + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + + const targetWindow = getActiveWindow(); + if (!isAuxiliaryWindow(targetWindow.window)) { + return; // Currently, we only support toggling always on top for auxiliary windows + } + + return nativeHostService.toggleWindowAlwaysOnTop({ targetWindowId: getActiveWindow().vscodeWindowId }); + } +} + +export class EnableWindowAlwaysOnTopAction extends Action2 { + + static readonly ID = 'workbench.action.enableWindowAlwaysOnTop'; + + constructor() { + super({ + id: EnableWindowAlwaysOnTopAction.ID, + title: localize('enableWindowAlwaysOnTop', "Set Always on Top"), + icon: Codicon.pin, + menu: { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext.toNegated(), IsAuxiliaryTitleBarContext), + order: 1 + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + + const targetWindow = getActiveWindow(); + if (!isAuxiliaryWindow(targetWindow.window)) { + return; // Currently, we only support toggling always on top for auxiliary windows + } + + return nativeHostService.setWindowAlwaysOnTop(true, { targetWindowId: targetWindow.vscodeWindowId }); + } +} + +export class DisableWindowAlwaysOnTopAction extends Action2 { + + static readonly ID = 'workbench.action.disableWindowAlwaysOnTop'; + + constructor() { + super({ + id: DisableWindowAlwaysOnTopAction.ID, + title: localize('disableWindowAlwaysOnTop', "Unset Always on Top"), + icon: Codicon.pin, + toggled: { condition: IsWindowAlwaysOnTopContext, icon: Codicon.pinned }, + menu: { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.and(IsWindowAlwaysOnTopContext, IsAuxiliaryTitleBarContext), + order: 1 + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const nativeHostService = accessor.get(INativeHostService); + + const targetWindow = getActiveWindow(); + if (!isAuxiliaryWindow(targetWindow.window)) { + return; // Currently, we only support toggling always on top for auxiliary windows + } + + return nativeHostService.setWindowAlwaysOnTop(false, { targetWindowId: targetWindow.vscodeWindowId }); + } +} diff --git a/code/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/code/src/vs/workbench/electron-sandbox/desktop.contribution.ts index d57c6d6e3ac..88e0091fe2c 100644 --- a/code/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/code/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -10,7 +10,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { KeyMod, KeyCode } from '../../base/common/keyCodes.js'; import { isLinux, isMacintosh, isWindows } from '../../base/common/platform.js'; import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ReloadWindowWithExtensionsDisabledAction, OpenUserDataFolderAction, ShowGPUInfoAction } from './actions/developerActions.js'; -import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from './actions/windowActions.js'; +import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler, ToggleWindowAlwaysOnTopAction, DisableWindowAlwaysOnTopAction, EnableWindowAlwaysOnTopAction } from './actions/windowActions.js'; import { ContextKeyExpr } from '../../platform/contextkey/common/contextkey.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../platform/keybinding/common/keybindingsRegistry.js'; import { CommandsRegistry } from '../../platform/commands/common/commands.js'; @@ -43,6 +43,9 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri registerAction2(SwitchWindowAction); registerAction2(QuickSwitchWindowAction); registerAction2(CloseWindowAction); + registerAction2(ToggleWindowAlwaysOnTopAction); + registerAction2(EnableWindowAlwaysOnTopAction); + registerAction2(DisableWindowAlwaysOnTopAction); if (isMacintosh) { // macOS: behave like other native apps that have documents diff --git a/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index cfd13ca9d03..07e8bc67405 100644 --- a/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -27,6 +27,7 @@ import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/ import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { CodeWindow, mainWindow } from '../../../../base/browser/window.js'; +import { IsWindowAlwaysOnTopContext } from '../../../common/contextkeys.js'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -80,6 +81,20 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { super(id, targetWindow, editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, editorGroupService, editorService, menuService, keybindingService); this.bigSurOrNewer = isBigSurOrNewer(environmentService.os.release); + + this.handleWindowsAlwaysOnTop(targetWindow.vscodeWindowId); + } + + private async handleWindowsAlwaysOnTop(targetWindowId: number): Promise { + const isWindowAlwaysOnTopContext = IsWindowAlwaysOnTopContext.bindTo(this.scopedContextKeyService); + + this._register(this.nativeHostService.onDidChangeWindowAlwaysOnTop(({ windowId, alwaysOnTop }) => { + if (windowId === targetWindowId) { + isWindowAlwaysOnTopContext.set(alwaysOnTop); + } + })); + + isWindowAlwaysOnTopContext.set(await this.nativeHostService.isWindowAlwaysOnTop({ targetWindowId })); } protected override onMenubarVisibilityChanged(visible: boolean): void { diff --git a/code/src/vs/workbench/electron-sandbox/window.ts b/code/src/vs/workbench/electron-sandbox/window.ts index d306dbe5bdc..a1b0dc1295e 100644 --- a/code/src/vs/workbench/electron-sandbox/window.ts +++ b/code/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'; @@ -68,7 +68,7 @@ import { Codicon } from '../../base/common/codicons.js'; import { IUriIdentityService } from '../../platform/uriIdentity/common/uriIdentity.js'; import { IPreferencesService } from '../services/preferences/common/preferences.js'; import { IUtilityProcessWorkerWorkbenchService } from '../services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.js'; -import { registerWindowDriver } from '../services/driver/electron-sandbox/driver.js'; +import { registerWindowDriver } from '../services/driver/browser/driver.js'; import { mainWindow } from '../../base/browser/window.js'; import { BaseWindow } from '../browser/window.js'; import { IHostService } from '../services/host/browser/host.js'; @@ -680,7 +680,7 @@ export class NativeWindow extends BaseWindow { // Smoke Test Driver if (this.environmentService.enableSmokeTestDriver) { - this.setupDriver(); + registerWindowDriver(this.instantiationService); } } @@ -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({ @@ -764,25 +738,6 @@ export class NativeWindow extends BaseWindow { }, () => shellEnv, () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667')); } - private setupDriver(): void { - const that = this; - let pendingQuit = false; - - registerWindowDriver(this.instantiationService, { - async exitApplication(): Promise { - if (pendingQuit) { - that.logService.info('[driver] not handling exitApplication() due to pending quit() call'); - return; - } - - that.logService.info('[driver] handling exitApplication()'); - - pendingQuit = true; - return that.nativeHostService.quit(); - } - }); - } - async resolveExternalUri(uri: URI, options?: OpenOptions): Promise { let queryTunnel: RemoteTunnel | string | undefined; if (options?.allowTunneling) { diff --git a/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts b/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts new file mode 100644 index 00000000000..f5d3a3b50bc --- /dev/null +++ b/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IAiSettingsSearchService = createDecorator('IAiSettingsSearchService'); + +export enum AiSettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + +export interface AiSettingsSearchResult { + query: string; + kind: AiSettingsSearchResultKind; + settings: string[]; +} + +export interface AiSettingsSearchProviderOptions { + limit: number; +} + +export interface IAiSettingsSearchService { + readonly _serviceBrand: undefined; + + // Called from the Settings editor + isEnabled(): boolean; + startSearch(query: string, token: CancellationToken): void; + getEmbeddingsResults(query: string, token: CancellationToken): Promise; + getLLMRankedResults(query: string, token: CancellationToken): Promise; + + // Called from the main thread + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable; + handleSearchResult(results: AiSettingsSearchResult): void; +} + +export interface IAiSettingsSearchProvider { + searchSettings(query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): void; +} diff --git a/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts b/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts new file mode 100644 index 00000000000..d34ec08f301 --- /dev/null +++ b/code/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind, IAiSettingsSearchProvider, IAiSettingsSearchService } from './aiSettingsSearch.js'; + +export class AiSettingsSearchService implements IAiSettingsSearchService { + readonly _serviceBrand: undefined; + private static readonly MAX_PICKS = 5; + + private _providers: IAiSettingsSearchProvider[] = []; + private _llmRankedResultsPromises: Map> = new Map(); + private _embeddingsResultsPromises: Map> = new Map(); + + isEnabled(): boolean { + return this._providers.length > 0; + } + + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable { + this._providers.push(provider); + return { + dispose: () => { + const index = this._providers.indexOf(provider); + if (index !== -1) { + this._providers.splice(index, 1); + } + } + }; + } + + startSearch(query: string, token: CancellationToken): void { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + this._providers.forEach(provider => provider.searchSettings(query, { limit: AiSettingsSearchService.MAX_PICKS }, token)); + } + + async getEmbeddingsResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._embeddingsResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + async getLLMRankedResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._llmRankedResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + handleSearchResult(result: AiSettingsSearchResult): void { + if (!this.isEnabled()) { + return; + } + + if (result.kind === AiSettingsSearchResultKind.EMBEDDED) { + const promise = this._embeddingsResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._embeddingsResultsPromises.delete(result.query); + } + } else if (result.kind === AiSettingsSearchResultKind.LLM_RANKED) { + const promise = this._llmRankedResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._llmRankedResultsPromises.delete(result.query); + } + } + } +} + +registerSingleton(IAiSettingsSearchService, AiSettingsSearchService, InstantiationType.Delayed); diff --git a/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 276e603077f..b11cd64b76e 100644 --- a/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/code/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -21,7 +21,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { DEFAULT_AUX_WINDOW_SIZE, IRectangle, WindowMinimumSize } from '../../../../platform/window/common/window.js'; +import { DEFAULT_AUX_WINDOW_SIZE, DEFAULT_COMPACT_AUX_WINDOW_SIZE, IRectangle, WindowMinimumSize } from '../../../../platform/window/common/window.js'; import { BaseWindow } from '../../../browser/window.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IHostService } from '../../host/browser/host.js'; @@ -42,9 +42,11 @@ export enum AuxiliaryWindowMode { export interface IAuxiliaryWindowOpenOptions { readonly bounds?: Partial; + readonly compact?: boolean; readonly mode?: AuxiliaryWindowMode; readonly zoomLevel?: number; + readonly alwaysOnTop?: boolean; readonly nativeTitlebar?: boolean; readonly disableFullscreen?: boolean; @@ -78,6 +80,8 @@ export interface IAuxiliaryWindow extends IDisposable { readonly window: CodeWindow; readonly container: HTMLElement; + updateOptions(options: { compact: boolean } | undefined): void; + layout(): void; createState(): IAuxiliaryWindowOpenOptions; @@ -104,6 +108,8 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { readonly whenStylesHaveLoaded: Promise; + private compact = false; + constructor( readonly window: CodeWindow, readonly container: HTMLElement, @@ -119,6 +125,10 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { this.registerListeners(); } + updateOptions(options: { compact: boolean }): void { + this.compact = options.compact; + } + private registerListeners(): void { this._register(addDisposableListener(this.window, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.handleBeforeUnload(e))); this._register(addDisposableListener(this.window, EventType.UNLOAD, () => this.handleUnload())); @@ -208,7 +218,8 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { width: this.window.outerWidth, height: this.window.outerHeight }, - zoomLevel: getZoomLevel(this.window) + zoomLevel: getZoomLevel(this.window), + compact: this.compact }; } @@ -227,8 +238,6 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili declare readonly _serviceBrand: undefined; - private static readonly DEFAULT_SIZE = DEFAULT_AUX_WINDOW_SIZE; - private static WINDOW_IDS = getWindowId(mainWindow) + 1; // start from the main window ID + 1 private readonly _onDidOpenAuxiliaryWindow = this._register(new Emitter()); @@ -263,6 +272,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili const { container, stylesLoaded } = this.createContainer(targetWindow, containerDisposables, options); const auxiliaryWindow = this.createAuxiliaryWindow(targetWindow, container, stylesLoaded); + auxiliaryWindow.updateOptions({ compact: options?.compact ?? false }); const registryDisposables = new DisposableStore(); this.windows.set(targetWindow.vscodeWindowId, auxiliaryWindow); @@ -309,8 +319,10 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili height: activeWindow.outerHeight }; - const width = Math.max(options?.bounds?.width ?? BrowserAuxiliaryWindowService.DEFAULT_SIZE.width, WindowMinimumSize.WIDTH); - const height = Math.max(options?.bounds?.height ?? BrowserAuxiliaryWindowService.DEFAULT_SIZE.height, WindowMinimumSize.HEIGHT); + const defaultSize = options?.compact ? DEFAULT_COMPACT_AUX_WINDOW_SIZE : DEFAULT_AUX_WINDOW_SIZE; + + const width = Math.max(options?.bounds?.width ?? defaultSize.width, WindowMinimumSize.WIDTH); + const height = Math.max(options?.bounds?.height ?? defaultSize.height, WindowMinimumSize.HEIGHT); let newWindowBounds: IRectangle = { x: options?.bounds?.x ?? Math.max(activeWindowBounds.x + activeWindowBounds.width / 2 - width / 2, 0), @@ -339,6 +351,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili // non-standard properties options?.nativeTitlebar ? 'window-native-titlebar=yes' : undefined, options?.disableFullscreen ? 'window-disable-fullscreen=yes' : undefined, + options?.alwaysOnTop ? 'window-always-on-top=yes' : undefined, options?.mode === AuxiliaryWindowMode.Maximized ? 'window-maximized=yes' : undefined, options?.mode === AuxiliaryWindowMode.Fullscreen ? 'window-fullscreen=yes' : undefined ]); diff --git a/code/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/code/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 6b98b66e621..2c6f4addc8e 100644 --- a/code/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/code/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -34,6 +34,7 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { private skipUnloadConfirmation = false; private maximized = false; + private alwaysOnTop = false; constructor( window: CodeWindow, @@ -55,6 +56,7 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { } this.handleFullScreenState(); + this.handleAlwaysOnTopState(); } private handleMaximizedState(): void { @@ -75,6 +77,18 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { })); } + private handleAlwaysOnTopState(): void { + (async () => { + this.alwaysOnTop = await this.nativeHostService.isWindowAlwaysOnTop({ targetWindowId: this.window.vscodeWindowId }); + })(); + + this._register(this.nativeHostService.onDidChangeWindowAlwaysOnTop(({ windowId, alwaysOnTop }) => { + if (windowId === this.window.vscodeWindowId) { + this.alwaysOnTop = alwaysOnTop; + } + })); + } + private async handleFullScreenState(): Promise { const fullscreen = await this.nativeHostService.isFullScreen({ targetWindowId: this.window.vscodeWindowId }); if (fullscreen) { @@ -113,7 +127,8 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { return { ...state, bounds: state.bounds, - mode: this.maximized ? AuxiliaryWindowMode.Maximized : fullscreen ? AuxiliaryWindowMode.Fullscreen : AuxiliaryWindowMode.Normal + mode: this.maximized ? AuxiliaryWindowMode.Maximized : fullscreen ? AuxiliaryWindowMode.Fullscreen : AuxiliaryWindowMode.Normal, + alwaysOnTop: this.alwaysOnTop }; } } @@ -156,7 +171,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.createContainer(auxiliaryWindow, disposables); } - protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier,): AuxiliaryWindow { + protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier): AuxiliaryWindow { return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService, this.environmentService, this.dialogService); } } diff --git a/code/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts b/code/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts index b9a2f688502..0ee5260206a 100644 --- a/code/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts +++ b/code/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts @@ -20,6 +20,10 @@ export class NativeClipboardService implements IClipboardService { @INativeHostService private readonly nativeHostService: INativeHostService ) { } + async triggerPaste(targetWindowId: number): Promise { + return this.nativeHostService.triggerPaste({ targetWindowId }); + } + async readImage(): Promise { return this.nativeHostService.readImage(); } diff --git a/code/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/code/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index bd2e3c3959c..c192b3a38a7 100644 --- a/code/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/code/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -17,7 +17,7 @@ import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } f import { ILabelService } from '../../../../platform/label/common/label.js'; import { IInputOptions, IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkspaceContextService, IWorkspaceFolderData, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; import { IEditorService } from '../../editor/common/editorService.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; @@ -44,7 +44,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR editorService: IEditorService, private readonly configurationService: IConfigurationService, private readonly commandService: ICommandService, - private readonly workspaceContextService: IWorkspaceContextService, + workspaceContextService: IWorkspaceContextService, private readonly quickInputService: IQuickInputService, private readonly labelService: ILabelService, private readonly pathService: IPathService, @@ -144,10 +144,6 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR } override async resolveWithInteractionReplace(folder: IWorkspaceFolderData | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise { - // First resolve any non-interactive variables and any contributed variables - config = await this.resolveAsync(folder, config); - - // Then resolve input variables in the order in which they are encountered const parsed = ConfigurationResolverExpression.parse(config); await this.resolveWithInteraction(folder, parsed, section, variables, target); @@ -180,9 +176,14 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR else if (this._contributedVariables.has(variable.inner)) { result = { value: await this._contributedVariables.get(variable.inner)!() }; } - // Not something we can handle else { - continue; + // Fallback to parent evaluation + const resolvedValue = await this.evaluateSingleVariable(variable, folder?.uri); + if (resolvedValue === undefined) { + // Not something we can handle + continue; + } + result = typeof resolvedValue === 'string' ? { value: resolvedValue } : resolvedValue; } if (result === undefined) { @@ -197,7 +198,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR } private async resolveInputs(folder: IWorkspaceFolderData | undefined, section: string, target?: ConfigurationTarget): Promise { - if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !section) { + if (!section) { return undefined; } @@ -276,7 +277,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (typeof resolvedInput === 'string') { this.storeInputLru(defaultValueMap.set(defaultValueKey, resolvedInput)); } - return resolvedInput ? { value: resolvedInput as string, input: info } : undefined; + return resolvedInput !== undefined ? { value: resolvedInput as string, input: info } : undefined; }); } diff --git a/code/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts b/code/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts index ff9cb66eaa8..1cabb263d78 100644 --- a/code/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts +++ b/code/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts @@ -46,6 +46,7 @@ interface IConfigurationResolverExpression { type PropertyLocation = { object: any; propertyName: string | number; + replaceKeyName?: boolean; }; export interface IResolvedValue { @@ -67,6 +68,11 @@ export class ConfigurationResolverExpression implements IConfigurationResolve private locations = new Map(); private root: T; private stringRoot: boolean; + /** + * Callbacks when a new replacement is made, so that nested resolutions from + * `expr.unresolved()` can be fulfilled in the same iteration. + */ + private newReplacementNotifiers = new Set<(r: Replacement) => void>(); private constructor(object: T) { // If the input is a string, wrap it in an object so we can use the same logic @@ -98,11 +104,10 @@ export class ConfigurationResolverExpression implements IConfigurationResolve private applyPlatformSpecificKeys() { const config = this.root as any; // already cloned by ctor, safe to change const key = isWindows ? 'windows' : isMacintosh ? 'osx' : isLinux ? 'linux' : undefined; - if (key === undefined || !config || typeof config !== 'object' || !config.hasOwnProperty(key)) { - return; - } - Object.keys(config[key]).forEach(k => config[k] = config[key][k]); + if (key && config && typeof config === 'object' && config.hasOwnProperty(key)) { + Object.keys(config[key]).forEach(k => config[k] = config[key][k]); + } delete config.windows; delete config.osx; @@ -174,9 +179,14 @@ export class ConfigurationResolverExpression implements IConfigurationResolve this.parseObject(value); } } + + // only after all values are marked for replacement, we can collect keys that have to be replaced + for (const [key] of Object.entries(obj)) { + this.parseString(obj, key, key, true); + } } - private parseString(object: any, propertyName: string | number, value: string): void { + private parseString(object: any, propertyName: string | number, value: string, replaceKeyName?: boolean, replacementPath?: string[]): void { let pos = 0; while (pos < value.length) { const match = value.indexOf('${', pos); @@ -185,18 +195,53 @@ export class ConfigurationResolverExpression implements IConfigurationResolve } const parsed = this.parseVariable(value, match); if (parsed) { + pos = parsed.end + 1; + if (replacementPath?.includes(parsed.replacement.id)) { + continue; + } + const locations = this.locations.get(parsed.replacement.id) || { locations: [], replacement: parsed.replacement }; - locations.locations.push({ object, propertyName }); + const newLocation: PropertyLocation = { object, propertyName, replaceKeyName }; + locations.locations.push(newLocation); this.locations.set(parsed.replacement.id, locations); - pos = parsed.end + 1; + + if (locations.resolved) { + this._resolveAtLocation(parsed.replacement, newLocation, locations.resolved, replacementPath); + } else { + this.newReplacementNotifiers.forEach(n => n(parsed.replacement)); + } } else { pos = match + 2; } } } - public unresolved(): Iterable { - return Iterable.map(Iterable.filter(this.locations.values(), l => l.resolved === undefined), l => l.replacement); + public *unresolved(): Iterable { + const newReplacements = new Map(); + const notifier = (replacement: Replacement) => { + newReplacements.set(replacement.id, replacement); + }; + + for (const location of this.locations.values()) { + if (location.resolved === undefined) { + newReplacements.set(location.replacement.id, location.replacement); + } + } + + this.newReplacementNotifiers.add(notifier); + + while (true) { + const next = Iterable.first(newReplacements); + if (!next) { + break; + } + + const [key, value] = next; + yield value; + newReplacements.delete(key); + } + + this.newReplacementNotifiers.delete(notifier); } public resolved(): Iterable<[Replacement, IResolvedValue]> { @@ -213,14 +258,36 @@ export class ConfigurationResolverExpression implements IConfigurationResolve return; } + location.resolved = data; + if (data.value !== undefined) { - for (const { object, propertyName } of location.locations || []) { - const newValue = object[propertyName].replaceAll(replacement.id, data.value); - object[propertyName] = newValue; + for (const l of location.locations || Iterable.empty()) { + this._resolveAtLocation(replacement, l, data); } } + } - location.resolved = data; + private _resolveAtLocation(replacement: Replacement, { replaceKeyName, propertyName, object }: PropertyLocation, data: IResolvedValue, path: string[] = []) { + if (data.value === undefined) { + return; + } + + // avoid recursive resolution, e.g. ${env:FOO} -> ${env:BAR}=${env:FOO} + path.push(replacement.id); + + // note: in nested `this.parseString`, parse only the new substring for any replacements, don't reparse the whole string + if (replaceKeyName && typeof propertyName === 'string') { + const value = object[propertyName]; + const newKey = propertyName.replaceAll(replacement.id, data.value); + delete object[propertyName]; + object[newKey] = value; + this.parseString(object, newKey, data.value, true, path); + } else { + this.parseString(object, propertyName, data.value, false, path); + object[propertyName] = object[propertyName].replaceAll(replacement.id, data.value); + } + + path.pop(); } public toObject(): T { diff --git a/code/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/code/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index dac0976ce1b..3b2c9d392a8 100644 --- a/code/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/code/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -69,15 +69,11 @@ export abstract class AbstractVariableResolverService implements IConfigurationR public async resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolderData | undefined, value: string): Promise { const expr = ConfigurationResolverExpression.parse(value); - const env: Environment = { - env: this.prepareEnv(environment), - userHome: undefined - }; for (const replacement of expr.unresolved()) { - const resolvedValue = await this.evaluateSingleVariable(env, replacement, folder?.uri); + const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri, environment); if (resolvedValue !== undefined) { - expr.resolve(replacement, resolvedValue); + expr.resolve(replacement, String(resolvedValue)); } } @@ -87,15 +83,10 @@ export abstract class AbstractVariableResolverService implements IConfigurationR public async resolveAsync(folder: IWorkspaceFolderData | undefined, config: T): Promise ? R : T> { const expr = ConfigurationResolverExpression.parse(config); - const environment: Environment = { - env: await this._envVariablesPromise, - userHome: await this._userHomePromise - }; - for (const replacement of expr.unresolved()) { - const resolvedValue = await this.evaluateSingleVariable(environment, replacement, folder?.uri); + const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri); if (resolvedValue !== undefined) { - expr.resolve(replacement, resolvedValue); + expr.resolve(replacement, String(resolvedValue)); } } @@ -123,7 +114,14 @@ export abstract class AbstractVariableResolverService implements IConfigurationR return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath; } - private async evaluateSingleVariable(environment: Environment, replacement: Replacement, folderUri: uri | undefined, commandValueMapping?: IStringDictionary): Promise { + protected async evaluateSingleVariable(replacement: Replacement, folderUri: uri | undefined, processEnvironment?: IProcessEnvironment, commandValueMapping?: IStringDictionary): Promise { + + + const environment: Environment = { + env: (processEnvironment !== undefined) ? this.prepareEnv(processEnvironment) : await this._envVariablesPromise, + userHome: (processEnvironment !== undefined) ? undefined : await this._userHomePromise + }; + const { name: variable, arg: argument } = replacement; // common error handling for all variables that require an open editor diff --git a/code/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/code/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index 880781bfc96..68d24b67cf9 100644 --- a/code/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/code/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -101,6 +101,26 @@ suite('Configuration Resolver Service', () => { } }); + test('does not preserve platform config even when not matched', async () => { + const obj = { + program: 'osx.sh', + windows: { + program: 'windows.exe' + }, + linux: { + program: 'linux.sh' + } + }; + const config: any = await configurationResolverService!.resolveAsync(workspace, obj); + + const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined; + + assert.strictEqual(config.windows, undefined); + assert.strictEqual(config.osx, undefined); + assert.strictEqual(config.linux, undefined); + assert.strictEqual(config.program, expected); + }); + test('apples platform specific config', async () => { const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined; const obj = { @@ -259,6 +279,17 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); + test('inlines an array (#245718)', async () => { + const configurationService: IConfigurationService = new TestConfigurationService({ + editor: { + fontFamily: ['foo', 'bar'] + }, + }); + + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService())); + assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo,bar xyz'); + }); + test('substitute configuration variable with undefined workspace folder', async () => { const configurationService: IConfigurationService = new TestConfigurationService({ editor: { @@ -306,6 +337,17 @@ suite('Configuration Resolver Service', () => { } }); + test('recursively resolve variables', async () => { + const configurationService = new TestConfigurationService({ + key1: 'key1=${config:key2}', + key2: 'key2=${config:key3}', + key3: 'we did it!', + }); + + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(envVariables), disposables.add(new TestEditorServiceWithActiveEditor()), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService, disposables.add(new TestStorageService())); + assert.strictEqual(await service.resolveAsync(workspace, '${config:key1}'), 'key1=key2=we did it!'); + }); + test('substitute many env variable and a configuration variable', async () => { const configurationService = new TestConfigurationService({ editor: { @@ -673,6 +715,43 @@ suite('Configuration Resolver Service', () => { const resolvedResult = await configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2'); }); + + test('substitution in object key', async () => { + + const configuration = { + 'name': 'Test', + 'mappings': { + 'pos1': 'value1', + '${workspaceFolder}/test1': '${workspaceFolder}/test2', + 'pos3': 'value3' + } + }; + + return configurationResolverService!.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => { + + if (platform.isWindows) { + assert.deepStrictEqual({ ...result }, { + 'name': 'Test', + 'mappings': { + 'pos1': 'value1', + '\\VSCode\\workspaceLocation/test1': '\\VSCode\\workspaceLocation/test2', + 'pos3': 'value3' + } + }); + } else { + assert.deepStrictEqual({ ...result }, { + 'name': 'Test', + 'mappings': { + 'pos1': 'value1', + '/VSCode/workspaceLocation/test1': '/VSCode/workspaceLocation/test2', + 'pos3': 'value3' + } + }); + } + + assert.strictEqual(0, mockCommandService.callCount); + }); + }); }); @@ -911,4 +990,24 @@ suite('ConfigurationResolverExpression', () => { assert.strictEqual(unresolved[0].name, 'env'); assert.strictEqual(unresolved[0].arg, 'HOME${env:USER}'); }); + + test('resolves nested values', () => { + const expr = ConfigurationResolverExpression.parse({ + name: '${env:REDIRECTED}', + 'key that is ${env:REDIRECTED}': 'cool!', + }); + + for (const r of expr.unresolved()) { + if (r.arg === 'REDIRECTED') { + expr.resolve(r, 'username: ${env:USERNAME}'); + } else if (r.arg === 'USERNAME') { + expr.resolve(r, 'testuser'); + } + } + + assert.deepStrictEqual(expr.toObject(), { + name: 'username: testuser', + 'key that is username: testuser': 'cool!' + }); + }); }); diff --git a/code/src/vs/workbench/services/driver/browser/driver.ts b/code/src/vs/workbench/services/driver/browser/driver.ts index bb11f37a641..6bd4ead2265 100644 --- a/code/src/vs/workbench/services/driver/browser/driver.ts +++ b/code/src/vs/workbench/services/driver/browser/driver.ts @@ -259,11 +259,6 @@ export class BrowserWindowDriver implements IWindowDriver { return { x, y }; } - - async exitApplication(): Promise { - // No-op in web - } - } export function registerWindowDriver(instantiationService: IInstantiationService): void { diff --git a/code/src/vs/workbench/services/driver/common/driver.ts b/code/src/vs/workbench/services/driver/common/driver.ts index 4cc5952df5d..5194ee5dddd 100644 --- a/code/src/vs/workbench/services/driver/common/driver.ts +++ b/code/src/vs/workbench/services/driver/common/driver.ts @@ -45,6 +45,5 @@ export interface IWindowDriver { getLocalizedStrings(): Promise; getLogs(): Promise; whenWorkbenchRestored(): Promise; - exitApplication(): Promise; } //*END diff --git a/code/src/vs/workbench/services/driver/electron-sandbox/driver.ts b/code/src/vs/workbench/services/driver/electron-sandbox/driver.ts deleted file mode 100644 index 79740006134..00000000000 --- a/code/src/vs/workbench/services/driver/electron-sandbox/driver.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { mainWindow } from '../../../../base/browser/window.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { BrowserWindowDriver } from '../browser/driver.js'; -import { ILifecycleService } from '../../lifecycle/common/lifecycle.js'; - -interface INativeWindowDriverHelper { - exitApplication(): Promise; -} - -class NativeWindowDriver extends BrowserWindowDriver { - - constructor( - private readonly helper: INativeWindowDriverHelper, - @IFileService fileService: IFileService, - @IEnvironmentService environmentService: IEnvironmentService, - @ILifecycleService lifecycleService: ILifecycleService, - @ILogService logService: ILogService - ) { - super(fileService, environmentService, lifecycleService, logService); - } - - override exitApplication(): Promise { - return this.helper.exitApplication(); - } -} - -export function registerWindowDriver(instantiationService: IInstantiationService, helper: INativeWindowDriverHelper): void { - Object.assign(mainWindow, { driver: instantiationService.createInstance(NativeWindowDriver, helper) }); -} diff --git a/code/src/vs/workbench/services/editor/browser/editorService.ts b/code/src/vs/workbench/services/editor/browser/editorService.ts index b3a64ddfbda..3b68ec6f3fe 100644 --- a/code/src/vs/workbench/services/editor/browser/editorService.ts +++ b/code/src/vs/workbench/services/editor/browser/editorService.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IResourceEditorInput, IEditorOptions, EditorActivation, IResourceEditorInputIdentifier, ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, EditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, IFindEditorOptions, isResourceMergeEditorInput, IEditorWillOpenEvent, IEditorControl } from '../../../common/editor.js'; +import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, EditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, IFindEditorOptions, isResourceMergeEditorInput, IEditorWillOpenEvent, IEditorControl, ITextResourceDiffEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; import { ResourceMap, ResourceSet } from '../../../../base/common/map.js'; @@ -530,6 +530,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { openEditor(editor: IUntypedEditorInput, group?: PreferredGroup): Promise; openEditor(editor: IResourceEditorInput, group?: PreferredGroup): Promise; openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: PreferredGroup): Promise; + openEditor(editor: ITextResourceDiffEditorInput, group?: PreferredGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: PreferredGroup): Promise; openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise; async openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise { diff --git a/code/src/vs/workbench/services/editor/common/editorGroupFinder.ts b/code/src/vs/workbench/services/editor/common/editorGroupFinder.ts index a39a1224160..0ae0eaadb1c 100644 --- a/code/src/vs/workbench/services/editor/common/editorGroupFinder.ts +++ b/code/src/vs/workbench/services/editor/common/editorGroupFinder.ts @@ -92,7 +92,7 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer // Group: Aux Window else if (preferredGroup === AUX_WINDOW_GROUP) { - group = editorGroupService.createAuxiliaryEditorPart().then(group => group.activeGroup); + group = editorGroupService.createAuxiliaryEditorPart({ compact: options?.compact }).then(group => group.activeGroup); } // Group: Unspecified without a specific index to open diff --git a/code/src/vs/workbench/services/editor/common/editorGroupsService.ts b/code/src/vs/workbench/services/editor/common/editorGroupsService.ts index b340a83f1a3..1388eac0a75 100644 --- a/code/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/code/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -565,7 +565,7 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { * Opens a new window with a full editor part instantiated * in there at the optional position and size on screen. */ - createAuxiliaryEditorPart(options?: { bounds?: Partial }): Promise; + createAuxiliaryEditorPart(options?: { bounds?: Partial; compact?: boolean }): Promise; /** * Returns the instantiation service that is scoped to the @@ -894,8 +894,9 @@ export interface IEditorGroup { * Closes all editors from the group. This may trigger a confirmation dialog if * there are dirty editors and thus returns a promise as value. * - * @returns a promise when all editors are closed. + * @returns a promise if confirmation is needed when all editors are closed. */ + closeAllEditors(options: { excludeConfirming: true }): boolean; closeAllEditors(options?: ICloseAllEditorsOptions): Promise; /** diff --git a/code/src/vs/workbench/services/editor/common/editorService.ts b/code/src/vs/workbench/services/editor/common/editorService.ts index 3c406fcf104..2847f2d4afd 100644 --- a/code/src/vs/workbench/services/editor/common/editorService.ts +++ b/code/src/vs/workbench/services/editor/common/editorService.ts @@ -5,7 +5,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IResourceEditorInput, IEditorOptions, IResourceEditorInputIdentifier, ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, IFindEditorOptions, IEditorWillOpenEvent } from '../../../common/editor.js'; +import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, IFindEditorOptions, IEditorWillOpenEvent, ITextResourceDiffEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { Event } from '../../../../base/common/event.js'; import { IEditor, IDiffEditor } from '../../../../editor/common/editorCommon.js'; @@ -260,7 +260,7 @@ export interface IEditorService { */ openEditor(editor: IResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; - openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; + openEditor(editor: ITextResourceDiffEditorInput | IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; openEditor(editor: IUntypedEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; /** diff --git a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 10b674d60ea..b1baabfef30 100644 --- a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -348,7 +348,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = this._getUserEnablementState(extension.identifier); const isEnabled = this.isEnabledEnablementState(enablementState); - if (isMalicious(extension.identifier, this.getMaliciousExtensions())) { + if (isMalicious(extension.identifier, this.getMaliciousExtensions().map(e => ({ extensionOrPublisher: e })))) { enablementState = EnablementState.DisabledByMalicious; } @@ -716,7 +716,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private async checkForMaliciousExtensions(): Promise { try { const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); - const changed = this.storeMaliciousExtensions(extensionsControlManifest.malicious); + const changed = this.storeMaliciousExtensions(extensionsControlManifest.malicious.map(({ extensionOrPublisher }) => extensionOrPublisher)); if (changed) { this._onDidChangeExtensions([], [], false); } diff --git a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 85b6b47cd9e..01bf76f61fe 100644 --- a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -131,8 +131,8 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return super.updateMetadata(local, metadata, await this.getProfileLocation(extensionsProfileResource)); } - override async toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { - return super.toggleAppliationScope(local, await this.getProfileLocation(fromProfileLocation)); + override async toggleApplicationScope(local: ILocalExtension, fromProfileLocation: URI): Promise { + return super.toggleApplicationScope(local, await this.getProfileLocation(fromProfileLocation)); } override async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { diff --git a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 798de91c5ea..edfe3a2c557 100644 --- a/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -49,6 +49,7 @@ import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlCon import { verifiedPublisherIcon } from './extensionsIcons.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; +import { CommontExtensionManagementService } from '../../../../platform/extensionManagement/common/abstractExtensionManagementService.js'; const TrustedPublishersStorageKey = 'extensions.trustedPublishers'; @@ -56,7 +57,7 @@ function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): return extension.type === 'gallery'; } -export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { +export class ExtensionManagementService extends CommontExtensionManagementService implements IWorkbenchExtensionManagementService { declare readonly _serviceBrand: undefined; @@ -98,7 +99,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IConfigurationService protected readonly configurationService: IConfigurationService, - @IProductService protected readonly productService: IProductService, + @IProductService productService: IProductService, @IDownloadService protected readonly downloadService: IDownloadService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IDialogService private readonly dialogService: IDialogService, @@ -108,11 +109,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { - super(); + super(productService, allowedExtensionsService); this.defaultTrustedPublishers = productService.trustedExtensionPublishers ?? []; this.workspaceExtensionManagementService = this._register(this.instantiationService.createInstance(WorkspaceExtensionsManagementService)); @@ -382,7 +383,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject('No Servers'); } - async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { + override async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { if (isGalleryExtension(extension)) { return this.canInstallGalleryExtension(extension); } @@ -1112,10 +1113,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench await Promise.allSettled(this.servers.map(server => server.extensionManagementService.cleanUp())); } - toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { + toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise { const server = this.getServer(extension); if (server) { - return server.extensionManagementService.toggleAppliationScope(extension, fromProfileLocation); + return server.extensionManagementService.toggleApplicationScope(extension, fromProfileLocation); } throw new Error('Not Supported'); } diff --git a/code/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/code/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 1cde4cdfc68..605a3c09fab 100644 --- a/code/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/code/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/code/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/code/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index e7c4ef4ae5b..81a6a52a0c9 100644 --- a/code/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; -import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, IAllowedExtensionsService, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -55,7 +55,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag override async installFromGallery(extension: IGalleryExtension, installOptions: InstallOptions = {}): Promise { if (isUndefined(installOptions.donotVerifySignature)) { - const value = this.configurationService.getValue('extensions.verifySignature'); + const value = this.configurationService.getValue(VerifyExtensionSignatureConfigKey); installOptions.donotVerifySignature = isBoolean(value) ? !value : undefined; } const local = await this.doInstallFromGallery(extension, installOptions); diff --git a/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index b68956b47e0..1f5a36a652f 100644 --- a/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/code/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -154,7 +154,7 @@ suite('ExtensionEnablementService Test', () => { getInstalled: () => Promise.resolve(installed), async getExtensionsControlManifest(): Promise { return { - malicious, + malicious: malicious.map(e => ({ extensionOrPublisher: e })), deprecated: {}, search: [] }; diff --git a/code/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/code/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index 10486e47ffd..ed188f581ec 100644 --- a/code/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/code/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -26,7 +26,7 @@ export interface IReadOnlyExtensionDescriptionRegistry { getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined; } -export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescriptionRegistry { +export class ExtensionDescriptionRegistry extends Disposable implements IReadOnlyExtensionDescriptionRegistry { public static isHostExtension(extensionId: ExtensionIdentifier | string, myRegistry: ExtensionDescriptionRegistry, globalRegistry: ExtensionDescriptionRegistry): boolean { if (myRegistry.getExtensionDescription(extensionId)) { @@ -44,7 +44,7 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti return false; } - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); public readonly onDidChange = this._onDidChange.event; private _versionId: number = 0; @@ -57,6 +57,7 @@ export class ExtensionDescriptionRegistry implements IReadOnlyExtensionDescripti private readonly _activationEventsReader: IActivationEventsReader, extensionDescriptions: IExtensionDescription[] ) { + super(); this._extensionDescriptions = extensionDescriptions; this._initialize(); } diff --git a/code/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts b/code/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts index aa9cc6c629d..24348eb2260 100644 --- a/code/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts +++ b/code/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts @@ -28,6 +28,8 @@ suite('ExtensionDescriptionRegistry', () => { registry.deltaExtensions([extensionA2], [idA]); assert.deepStrictEqual(registry.getAllExtensionDescriptions(), [extensionA2]); + + registry.dispose(); }); function desc(id: ExtensionIdentifier, version: string, activationEvents: string[] = ['*']): IExtensionDescription { diff --git a/code/src/vs/workbench/services/host/browser/browserHostService.ts b/code/src/vs/workbench/services/host/browser/browserHostService.ts index 57cb6e482cf..66f4acaa3b0 100644 --- a/code/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/code/src/vs/workbench/services/host/browser/browserHostService.ts @@ -41,6 +41,8 @@ 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'; +import { IElementData } from '../../../../platform/native/common/native.js'; enum HostShutdownReason { @@ -96,6 +98,7 @@ export class BrowserHostService extends Disposable implements IHostService { this.registerListeners(); } + private registerListeners(): void { // Veto shutdown depending on `window.confirmBeforeClose` setting @@ -154,7 +157,7 @@ export class BrowserHostService extends Disposable implements IHostService { Event.map(focusTracker.onDidBlur, () => this.hasFocus, disposables), Event.map(visibilityTracker.event, () => this.hasFocus, disposables), Event.map(this.onDidChangeActiveWindow, () => this.hasFocus, disposables), - )(focus => emitter.fire(focus)); + )(focus => emitter.fire(focus), undefined, disposables); }, { window: mainWindow, disposables: this._store })); return Event.latch(emitter.event, undefined, this._store); @@ -587,7 +590,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 +636,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); @@ -649,6 +652,14 @@ export class BrowserHostService extends Disposable implements IHostService { } } + async getElementData(): Promise { + return undefined; + } + + async getBrowserId(): Promise { + return undefined; + } + //#endregion //#region Native Handle diff --git a/code/src/vs/workbench/services/host/browser/host.ts b/code/src/vs/workbench/services/host/browser/host.ts index 657c5ec4bad..c3a7161c2c6 100644 --- a/code/src/vs/workbench/services/host/browser/host.ts +++ b/code/src/vs/workbench/services/host/browser/host.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from '../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IElementData } from '../../../../platform/native/common/native.js'; import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; export const IHostService = createDecorator('hostService'); @@ -128,7 +130,9 @@ export interface IHostService { /** * Captures a screenshot. */ - getScreenshot(): Promise; + getScreenshot(rect?: IRectangle): Promise; + + getElementData(rect: IRectangle, token: CancellationToken,): Promise; //#endregion diff --git a/code/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/code/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index bda11474b2c..74c46564ae3 100644 --- a/code/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/code/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IHostService } from '../browser/host.js'; -import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IElementData, INativeHostService } from '../../../../platform/native/common/native.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; @@ -18,6 +18,8 @@ import { disposableWindowInterval, getActiveDocument, getWindowId, getWindowsCou import { memoize } from '../../../../base/common/decorators.js'; import { isAuxiliaryWindow } from '../../../../base/browser/window.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { ipcRenderer } from '../../../../base/parts/sandbox/electron-sandbox/globals.js'; class WorkbenchNativeHostService extends NativeHostService { @@ -29,6 +31,8 @@ class WorkbenchNativeHostService extends NativeHostService { } } +let cancelSelectionIdPool = 0; + class WorkbenchHostService extends Disposable implements IHostService { declare readonly _serviceBrand: undefined; @@ -193,8 +197,25 @@ class WorkbenchHostService extends Disposable implements IHostService { //#region Screenshots - getScreenshot(): Promise { - return this.nativeHostService.getScreenshot(); + getScreenshot(rect?: IRectangle): Promise { + return this.nativeHostService.getScreenshot(rect); + } + + async getElementData(rect: IRectangle, token: CancellationToken,): Promise { + const cancelSelectionId = cancelSelectionIdPool++; + const onCancelChannel = `vscode:cancelElementSelection${cancelSelectionId}`; + const disposable = token.onCancellationRequested(() => { + ipcRenderer.send(onCancelChannel, cancelSelectionId); + }); + try { + const elementData = await this.nativeHostService.getElementData(rect, token, cancelSelectionId); + return elementData; + } catch (error) { + disposable.dispose(); + throw new Error(`Native Host: Error getting element data: ${error}`); + } finally { + disposable.dispose(); + } } //#endregion diff --git a/code/src/vs/workbench/services/layout/browser/layoutService.ts b/code/src/vs/workbench/services/layout/browser/layoutService.ts index 41837c9e780..b14a2f93ac3 100644 --- a/code/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/code/src/vs/workbench/services/layout/browser/layoutService.ts @@ -223,9 +223,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Set part hidden or not in the target window. */ - setPartHidden(hidden: boolean, part: Exclude): void; - setPartHidden(hidden: boolean, part: Exclude, targetWindow: Window): void; - setPartHidden(hidden: boolean, part: Exclude, targetWindow: Window): void; + setPartHidden(hidden: boolean, part: Parts): void; /** * Maximizes the panel height if the panel is not already maximized. diff --git a/code/src/vs/workbench/services/notebook/common/notebookDocumentService.ts b/code/src/vs/workbench/services/notebook/common/notebookDocumentService.ts index e84341f7b30..6015927f614 100644 --- a/code/src/vs/workbench/services/notebook/common/notebookDocumentService.ts +++ b/code/src/vs/workbench/services/notebook/common/notebookDocumentService.ts @@ -66,7 +66,7 @@ export function generateMetadataUri(notebook: URI): URI { return notebook.with({ scheme: Schemas.vscodeNotebookMetadata, fragment }); } -export function extractCellOutputDetails(uri: URI): { notebook: URI; openIn: string; outputId?: string; cellFragment?: string; outputIndex?: number; cellHandle?: number } | undefined { +export function extractCellOutputDetails(uri: URI): { notebook: URI; openIn: string; outputId?: string; cellFragment?: string; outputIndex?: number; cellHandle?: number; cellIndex?: number } | undefined { if (uri.scheme !== Schemas.vscodeNotebookCellOutput) { return; } @@ -84,6 +84,7 @@ export function extractCellOutputDetails(uri: URI): { notebook: URI; openIn: str fragment: null, query: null, }); + const cellIndex = params.get('cellIndex') ? parseInt(params.get('cellIndex') || '', 10) : undefined; return { notebook: notebookUri, @@ -92,6 +93,7 @@ export function extractCellOutputDetails(uri: URI): { notebook: URI; openIn: str outputIndex: outputIndex, cellHandle: parsedCell?.handle, cellFragment: uri.fragment, + cellIndex: cellIndex, }; } diff --git a/code/src/vs/workbench/services/notification/common/notificationService.ts b/code/src/vs/workbench/services/notification/common/notificationService.ts index a61a1c3b6f0..24eeff23dc3 100644 --- a/code/src/vs/workbench/services/notification/common/notificationService.ts +++ b/code/src/vs/workbench/services/notification/common/notificationService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource, IStatusHandle } from '../../../../platform/notification/common/notification.js'; import { NotificationsModel, ChoiceAction, NotificationChangeType } from '../../../common/notifications.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IAction, Action } from '../../../../base/common/actions.js'; @@ -346,7 +346,7 @@ export class NotificationService extends Disposable implements INotificationServ return handle; } - status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable { + status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle { return this.model.showStatusMessage(message, options); } } diff --git a/code/src/vs/workbench/services/output/common/output.ts b/code/src/vs/workbench/services/output/common/output.ts index 0617017a0bc..f5f30494024 100644 --- a/code/src/vs/workbench/services/output/common/output.ts +++ b/code/src/vs/workbench/services/output/common/output.ts @@ -10,6 +10,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { LogLevel } from '../../../../platform/log/common/log.js'; import { Range } from '../../../../editor/common/core/range.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; /** * Mime type used by the output editor. @@ -271,16 +272,16 @@ export interface IOutputChannelRegistry { removeChannel(id: string): void; } -class OutputChannelRegistry implements IOutputChannelRegistry { +class OutputChannelRegistry extends Disposable implements IOutputChannelRegistry { private channels = new Map(); - private readonly _onDidRegisterChannel = new Emitter(); + private readonly _onDidRegisterChannel = this._register(new Emitter()); readonly onDidRegisterChannel = this._onDidRegisterChannel.event; - private readonly _onDidRemoveChannel = new Emitter(); + private readonly _onDidRemoveChannel = this._register(new Emitter()); readonly onDidRemoveChannel = this._onDidRemoveChannel.event; - private readonly _onDidUpdateChannelFiles = new Emitter(); + private readonly _onDidUpdateChannelFiles = this._register(new Emitter()); readonly onDidUpdateChannelSources = this._onDidUpdateChannelFiles.event; public registerChannel(descriptor: IOutputChannelDescriptor): void { diff --git a/code/src/vs/workbench/services/policies/common/accountPolicyService.ts b/code/src/vs/workbench/services/policies/common/accountPolicyService.ts index 1e5e80a55c0..9040c4f79f5 100644 --- a/code/src/vs/workbench/services/policies/common/accountPolicyService.ts +++ b/code/src/vs/workbench/services/policies/common/accountPolicyService.ts @@ -6,13 +6,13 @@ import { IStringDictionary } from '../../../../base/common/collections.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { AbstractPolicyService, IPolicyService, PolicyDefinition } from '../../../../platform/policy/common/policy.js'; -import { DefaultAccountService, IDefaultAccountService } from '../../accounts/common/defaultAccount.js'; +import { IDefaultAccountService } from '../../accounts/common/defaultAccount.js'; export class AccountPolicyService extends AbstractPolicyService implements IPolicyService { private chatPreviewFeaturesEnabled: boolean = true; constructor( @ILogService private readonly logService: ILogService, - @IDefaultAccountService private readonly defaultAccountService: DefaultAccountService + @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService ) { super(); diff --git a/code/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts b/code/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts new file mode 100644 index 00000000000..330b323631a --- /dev/null +++ b/code/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { DefaultAccountService, IDefaultAccount, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { AccountPolicyService } from '../../common/accountPolicyService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; +import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js'; + +const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { + enterprise: false, + sessionId: 'abc123', +}; + +suite('AccountPolicyService', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let policyService: AccountPolicyService; + let defaultAccountService: IDefaultAccountService; + let policyConfiguration: PolicyConfiguration; + const logService = new NullLogService(); + + const policyConfigurationNode: IConfigurationNode = { + 'id': 'policyConfiguration', + 'order': 1, + 'title': 'a', + 'type': 'object', + 'properties': { + 'setting.A': { + 'type': 'string', + 'default': 'defaultValueA', + policy: { + name: 'PolicySettingA', + minimumVersion: '1.0.0', + } + }, + 'setting.B': { + 'type': 'string', + 'default': 'defaultValueB', + policy: { + name: 'PolicySettingB', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: "policyValueB" + } + }, + 'setting.C': { + 'type': 'array', + 'default': ['defaultValueC1', 'defaultValueC2'], + policy: { + name: 'PolicySettingC', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: JSON.stringify(['policyValueC1', 'policyValueC2']), + } + }, + 'setting.D': { + 'type': 'boolean', + 'default': true, + policy: { + name: 'PolicySettingD', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: false, + } + }, + 'setting.E': { + 'type': 'boolean', + 'default': true, + } + } + }; + + + suiteSetup(() => Registry.as(Extensions.Configuration).registerConfiguration(policyConfigurationNode)); + suiteTeardown(() => Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode])); + + setup(async () => { + const defaultConfiguration = disposables.add(new DefaultConfiguration(new NullLogService())); + await defaultConfiguration.initialize(); + + defaultAccountService = disposables.add(new DefaultAccountService()); + policyService = disposables.add(new AccountPolicyService(logService, defaultAccountService)); + policyConfiguration = disposables.add(new PolicyConfiguration(defaultConfiguration, policyService, new NullLogService())); + + }); + + async function assertDefaultBehavior(defaultAccount: IDefaultAccount) { + defaultAccountService.setDefaultAccount(defaultAccount); + + await policyConfiguration.initialize(); + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + // No policy is set + assert.strictEqual(A, undefined); + assert.strictEqual(B, undefined); + assert.strictEqual(C, undefined); + assert.strictEqual(D, undefined); + } + + { + const B = policyConfiguration.configurationModel.getValue('setting.B'); + const C = policyConfiguration.configurationModel.getValue('setting.C'); + const D = policyConfiguration.configurationModel.getValue('setting.D'); + + assert.strictEqual(B, undefined); + assert.deepStrictEqual(C, undefined); + assert.strictEqual(D, undefined); + } + } + + + test('should initialize with default account', async () => { + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT }; + await assertDefaultBehavior(defaultAccount); + }); + + test('should initialize with default account and preview features enabled', async () => { + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: true }; + await assertDefaultBehavior(defaultAccount); + }); + + test('should initialize with default account and preview features disabled', async () => { + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccount(defaultAccount); + + await policyConfiguration.initialize(); + const actualConfigurationModel = policyConfiguration.configurationModel; + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + assert.strictEqual(A, undefined); // Not tagged with 'previewFeature' + assert.strictEqual(B, 'policyValueB'); + assert.strictEqual(C, JSON.stringify(['policyValueC1', 'policyValueC2'])); + assert.strictEqual(D, false); + } + + { + const B = actualConfigurationModel.getValue('setting.B'); + const C = actualConfigurationModel.getValue('setting.C'); + const D = actualConfigurationModel.getValue('setting.D'); + + assert.strictEqual(B, 'policyValueB'); + assert.deepStrictEqual(C, ['policyValueC1', 'policyValueC2']); + assert.strictEqual(D, false); + } + }); +}); diff --git a/code/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts b/code/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts new file mode 100644 index 00000000000..5617dcb820c --- /dev/null +++ b/code/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { DefaultAccountService, IDefaultAccount, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { AccountPolicyService } from '../../common/accountPolicyService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; +import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js'; +import { MultiplexPolicyService } from '../../common/multiplexPolicyService.js'; +import { FilePolicyService } from '../../../../../platform/policy/common/filePolicyService.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { FileService } from '../../../../../platform/files/common/fileService.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; + +const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { + enterprise: false, + sessionId: 'abc123', +}; + +suite('MultiplexPolicyService', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let policyService: MultiplexPolicyService; + let fileService: IFileService; + let defaultAccountService: IDefaultAccountService; + let policyConfiguration: PolicyConfiguration; + const logService = new NullLogService(); + + const policyFile = URI.file('policyFile').with({ scheme: 'vscode-tests' }); + const policyConfigurationNode: IConfigurationNode = { + 'id': 'policyConfiguration', + 'order': 1, + 'title': 'a', + 'type': 'object', + 'properties': { + 'setting.A': { + 'type': 'string', + 'default': 'defaultValueA', + policy: { + name: 'PolicySettingA', + minimumVersion: '1.0.0', + } + }, + 'setting.B': { + 'type': 'string', + 'default': 'defaultValueB', + policy: { + name: 'PolicySettingB', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: "policyValueB" + } + }, + 'setting.C': { + 'type': 'array', + 'default': ['defaultValueC1', 'defaultValueC2'], + policy: { + name: 'PolicySettingC', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: JSON.stringify(['policyValueC1', 'policyValueC2']), + } + }, + 'setting.D': { + 'type': 'boolean', + 'default': true, + policy: { + name: 'PolicySettingD', + minimumVersion: '1.0.0', + previewFeature: true, + defaultValue: false, + } + }, + 'setting.E': { + 'type': 'boolean', + 'default': true, + } + } + }; + + + suiteSetup(() => Registry.as(Extensions.Configuration).registerConfiguration(policyConfigurationNode)); + suiteTeardown(() => Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode])); + + setup(async () => { + const defaultConfiguration = disposables.add(new DefaultConfiguration(new NullLogService())); + await defaultConfiguration.initialize(); + + fileService = disposables.add(new FileService(new NullLogService())); + const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(policyFile.scheme, diskFileSystemProvider)); + + defaultAccountService = disposables.add(new DefaultAccountService()); + policyService = disposables.add(new MultiplexPolicyService([ + disposables.add(new FilePolicyService(policyFile, fileService, new NullLogService())), + disposables.add(new AccountPolicyService(logService, defaultAccountService)), + ], logService)); + policyConfiguration = disposables.add(new PolicyConfiguration(defaultConfiguration, policyService, new NullLogService())); + }); + + async function clear() { + // Reset + defaultAccountService.setDefaultAccount({ ...BASE_DEFAULT_ACCOUNT }); + await fileService.writeFile(policyFile, + VSBuffer.fromString( + JSON.stringify({}) + ) + ); + } + + test('no policy', async () => { + await clear(); + + await policyConfiguration.initialize(); + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + // No policy is set + assert.strictEqual(A, undefined); + assert.strictEqual(B, undefined); + assert.strictEqual(C, undefined); + assert.strictEqual(D, undefined); + } + + { + const A = policyConfiguration.configurationModel.getValue('setting.A'); + const B = policyConfiguration.configurationModel.getValue('setting.B'); + const C = policyConfiguration.configurationModel.getValue('setting.C'); + const D = policyConfiguration.configurationModel.getValue('setting.D'); + const E = policyConfiguration.configurationModel.getValue('setting.E'); + + assert.strictEqual(A, undefined); + assert.strictEqual(B, undefined); + assert.deepStrictEqual(C, undefined); + assert.strictEqual(D, undefined); + assert.strictEqual(E, undefined); + } + }); + + test('policy from file only', async () => { + await clear(); + + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT }; + defaultAccountService.setDefaultAccount(defaultAccount); + + await fileService.writeFile(policyFile, + VSBuffer.fromString( + JSON.stringify({ 'PolicySettingA': 'policyValueA' }) + ) + ); + + await policyConfiguration.initialize(); + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + assert.strictEqual(A, 'policyValueA'); + assert.strictEqual(B, undefined); + assert.strictEqual(C, undefined); + assert.strictEqual(D, undefined); + } + + { + const A = policyConfiguration.configurationModel.getValue('setting.A'); + const B = policyConfiguration.configurationModel.getValue('setting.B'); + const C = policyConfiguration.configurationModel.getValue('setting.C'); + const D = policyConfiguration.configurationModel.getValue('setting.D'); + const E = policyConfiguration.configurationModel.getValue('setting.E'); + + assert.strictEqual(A, 'policyValueA'); + assert.strictEqual(B, undefined); + assert.deepStrictEqual(C, undefined); + assert.strictEqual(D, undefined); + assert.strictEqual(E, undefined); + } + }); + + test('policy from default account only', async () => { + await clear(); + + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccount(defaultAccount); + + await fileService.writeFile(policyFile, + VSBuffer.fromString( + JSON.stringify({}) + ) + ); + + await policyConfiguration.initialize(); + const actualConfigurationModel = policyConfiguration.configurationModel; + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + assert.strictEqual(A, undefined); // Not tagged with 'previewFeature' + assert.strictEqual(B, 'policyValueB'); + assert.strictEqual(C, JSON.stringify(['policyValueC1', 'policyValueC2'])); + assert.strictEqual(D, false); + } + + { + const A = policyConfiguration.configurationModel.getValue('setting.A'); + const B = actualConfigurationModel.getValue('setting.B'); + const C = actualConfigurationModel.getValue('setting.C'); + const D = actualConfigurationModel.getValue('setting.D'); + + assert.strictEqual(A, undefined); + assert.strictEqual(B, 'policyValueB'); + assert.deepStrictEqual(C, ['policyValueC1', 'policyValueC2']); + assert.strictEqual(D, false); + } + }); + + test('policy from file and default account', async () => { + await clear(); + + const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, chat_preview_features_enabled: false }; + defaultAccountService.setDefaultAccount(defaultAccount); + + await fileService.writeFile(policyFile, + VSBuffer.fromString( + JSON.stringify({ 'PolicySettingA': 'policyValueA' }) + ) + ); + + await policyConfiguration.initialize(); + const actualConfigurationModel = policyConfiguration.configurationModel; + + { + const A = policyService.getPolicyValue('PolicySettingA'); + const B = policyService.getPolicyValue('PolicySettingB'); + const C = policyService.getPolicyValue('PolicySettingC'); + const D = policyService.getPolicyValue('PolicySettingD'); + + assert.strictEqual(A, 'policyValueA'); + assert.strictEqual(B, 'policyValueB'); + assert.strictEqual(C, JSON.stringify(['policyValueC1', 'policyValueC2'])); + assert.strictEqual(D, false); + } + + { + const A = actualConfigurationModel.getValue('setting.A'); + const B = actualConfigurationModel.getValue('setting.B'); + const C = actualConfigurationModel.getValue('setting.C'); + const D = actualConfigurationModel.getValue('setting.D'); + + assert.strictEqual(A, 'policyValueA'); + assert.strictEqual(B, 'policyValueB'); + assert.deepStrictEqual(C, ['policyValueC1', 'policyValueC2']); + assert.strictEqual(D, false); + } + }); +}); diff --git a/code/src/vs/workbench/services/preferences/browser/preferencesService.ts b/code/src/vs/workbench/services/preferences/browser/preferencesService.ts index c657835f97f..565c4dd6d03 100644 --- a/code/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/code/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -123,7 +123,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return workspace.configuration || workspace.folders[0].toResource(FOLDER_SETTINGS_PATH); } - createOrGetCachedSettingsEditor2Input(): SettingsEditor2Input { + private createOrGetCachedSettingsEditor2Input(): SettingsEditor2Input { if (!this._cachedSettingsEditor2Input || this._cachedSettingsEditor2Input.isDisposed()) { // Recreate the input if the user never opened the Settings editor, // or if they closed it and want to reopen it. diff --git a/code/src/vs/workbench/services/progress/browser/progressService.ts b/code/src/vs/workbench/services/progress/browser/progressService.ts index d8abbcda9d0..cb8831c7e77 100644 --- a/code/src/vs/workbench/services/progress/browser/progressService.ts +++ b/code/src/vs/workbench/services/progress/browser/progressService.ts @@ -572,6 +572,12 @@ export class ProgressService extends Disposable implements IProgressService { disposables.add(dialog); dialog.show().then(dialogResult => { + // 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); } diff --git a/code/src/vs/workbench/services/search/common/search.ts b/code/src/vs/workbench/services/search/common/search.ts index 88619390030..923021734e0 100644 --- a/code/src/vs/workbench/services/search/common/search.ts +++ b/code/src/vs/workbench/services/search/common/search.ts @@ -17,7 +17,7 @@ import { ITelemetryData } from '../../../../platform/telemetry/common/telemetry. import { Event } from '../../../../base/common/event.js'; import * as paths from '../../../../base/common/path.js'; import { isCancellationError } from '../../../../base/common/errors.js'; -import { GlobPattern, TextSearchCompleteMessageType } from './searchExtTypes.js'; +import { AISearchKeyword, GlobPattern, TextSearchCompleteMessageType } from './searchExtTypes.js'; import { isThenable } from '../../../../base/common/async.js'; import { ResourceSet } from '../../../../base/common/map.js'; @@ -263,6 +263,7 @@ export interface ISearchCompleteStats { export interface ISearchComplete extends ISearchCompleteStats { results: IFileMatch[]; exit?: SearchCompletionExitCode; + aiKeywords?: AISearchKeyword[]; } export const enum SearchCompletionExitCode { diff --git a/code/src/vs/workbench/services/search/common/searchExtTypes.ts b/code/src/vs/workbench/services/search/common/searchExtTypes.ts index decb405a400..595b0014095 100644 --- a/code/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/code/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -315,11 +315,28 @@ export class TextSearchContext2 { public lineNumber: number) { } } +/** +/** + * Keyword suggestion for AI search. + */ +export class AISearchKeyword { + /** + * @param keyword The keyword associated with the search. + */ + constructor(public keyword: string) { } +} + /** * A result payload for a text search, pertaining to matches within a single file. */ export type TextSearchResult2 = TextSearchMatch2 | TextSearchContext2; +/** + * A result payload for an AI search. + * This can be a {@link TextSearchMatch2 match} or a {@link AISearchKeyword keyword}. + * The result can be a match or a keyword. +*/ +export type AISearchResult = TextSearchResult2 | AISearchKeyword; /** * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions. diff --git a/code/src/vs/workbench/services/search/common/searchService.ts b/code/src/vs/workbench/services/search/common/searchService.ts index b0981b9891c..f52bcd99ed2 100644 --- a/code/src/vs/workbench/services/search/common/searchService.ts +++ b/code/src/vs/workbench/services/search/common/searchService.ts @@ -213,7 +213,8 @@ export class SearchService extends Disposable implements ISearchService { limitHit: completes[0] && completes[0].limitHit, stats: completes[0].stats, messages: arrays.coalesce(completes.flatMap(i => i.messages)).filter(arrays.uniqueFilter(message => message.type + message.text + message.trusted)), - results: completes.flatMap((c: ISearchComplete) => c.results) + results: completes.flatMap((c: ISearchComplete) => c.results), + aiKeywords: completes.flatMap((c: ISearchComplete) => c.aiKeywords).filter(keyword => keyword !== undefined), }; })(); diff --git a/code/src/vs/workbench/services/search/common/textSearchManager.ts b/code/src/vs/workbench/services/search/common/textSearchManager.ts index 59a10ed9024..92571aef883 100644 --- a/code/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/code/src/vs/workbench/services/search/common/textSearchManager.ts @@ -12,7 +12,7 @@ import * as resources from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { FolderQuerySearchTree } from './folderQuerySearchTree.js'; import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, excludeToGlobPattern, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider, ISearchRange, DEFAULT_TEXT_SEARCH_PREVIEW_OPTIONS } from './search.js'; -import { TextSearchComplete2, TextSearchMatch2, TextSearchProviderFolderOptions, TextSearchProvider2, TextSearchProviderOptions, TextSearchQuery2, TextSearchResult2, AITextSearchProvider } from './searchExtTypes.js'; +import { TextSearchComplete2, TextSearchMatch2, TextSearchProviderFolderOptions, TextSearchProvider2, TextSearchProviderOptions, TextSearchQuery2, TextSearchResult2, AITextSearchProvider, AISearchResult, AISearchKeyword } from './searchExtTypes.js'; export interface IFileUtils { readdir: (resource: URI) => Promise; @@ -46,7 +46,7 @@ export class TextSearchManager { return this.queryProviderPair.query; } - search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { + search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { const folderQueries = this.query.folderQueries || []; const tokenSource = new CancellationTokenSource(token); @@ -55,6 +55,10 @@ export class TextSearchManager { let isCanceled = false; const onResult = (result: TextSearchResult2, folderIdx: number) => { + if (result instanceof AISearchKeyword) { + // Already processed by the callback. + return; + } if (isCanceled) { return; } @@ -80,7 +84,7 @@ export class TextSearchManager { }; // For each root folder - this.doSearch(folderQueries, onResult, tokenSource.token).then(result => { + this.doSearch(folderQueries, onResult, tokenSource.token, onKeywordResult).then(result => { tokenSource.dispose(); this.collector!.flush(); @@ -121,7 +125,7 @@ export class TextSearchManager { return new TextSearchMatch2(result.uri, result.ranges.slice(0, size), result.previewText); } - private async doSearch(folderQueries: IFolderQuery[], onResult: (result: TextSearchResult2, folderIdx: number) => void, token: CancellationToken): Promise { + private async doSearch(folderQueries: IFolderQuery[], onResult: (result: TextSearchResult2, folderIdx: number) => void, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { const folderMappings: FolderQuerySearchTree = new FolderQuerySearchTree( folderQueries, (fq, i) => { @@ -133,31 +137,34 @@ export class TextSearchManager { const testingPs: Promise[] = []; const progress = { - report: (result: TextSearchResult2) => { - - if (result.uri === undefined) { - throw Error('Text search result URI is undefined. Please check provider implementation.'); - } - const folderQuery = folderMappings.findQueryFragmentAwareSubstr(result.uri)!; - const hasSibling = folderQuery.folder.scheme === Schemas.file ? - hasSiblingPromiseFn(() => { - return this.fileUtils.readdir(resources.dirname(result.uri)); - }) : - undefined; - - const relativePath = resources.relativePath(folderQuery.folder, result.uri); - if (relativePath) { - // This method is only async when the exclude contains sibling clauses - const included = folderQuery.queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); - if (isThenable(included)) { - testingPs.push( - included.then(isIncluded => { - if (isIncluded) { - onResult(result, folderQuery.folderIdx); - } - })); - } else if (included) { - onResult(result, folderQuery.folderIdx); + report: (result: TextSearchResult2 | AISearchResult) => { + if (result instanceof AISearchKeyword) { + onKeywordResult?.(result); + } else { + if (result.uri === undefined) { + throw Error('Text search result URI is undefined. Please check provider implementation.'); + } + const folderQuery = folderMappings.findQueryFragmentAwareSubstr(result.uri)!; + const hasSibling = folderQuery.folder.scheme === Schemas.file ? + hasSiblingPromiseFn(() => { + return this.fileUtils.readdir(resources.dirname(result.uri)); + }) : + undefined; + + const relativePath = resources.relativePath(folderQuery.folder, result.uri); + if (relativePath) { + // This method is only async when the exclude contains sibling clauses + const included = folderQuery.queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); + if (isThenable(included)) { + testingPs.push( + included.then(isIncluded => { + if (isIncluded) { + onResult(result, folderQuery.folderIdx); + } + })); + } else if (included) { + onResult(result, folderQuery.folderIdx); + } } } } diff --git a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index 8d4b120c568..51e1b5c24ab 100644 --- a/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/code/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -25,6 +25,7 @@ import { canExpandCompletionItem, SimpleSuggestDetailsOverlay, SimpleSuggestDeta import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import * as strings from '../../../../base/common/strings.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; +import { isWindows } from '../../../../base/common/platform.js'; const $ = dom.$; @@ -196,7 +197,7 @@ export class SimpleSuggestWidget, TI mouseSupport: false, multipleSelectionSupport: false, accessibilityProvider: { - getRole: () => 'listitem', + getRole: () => isWindows ? 'listitem' : 'option', getWidgetAriaLabel: () => localize('suggest', "Suggest"), getWidgetRole: () => 'listbox', getAriaLabel: (item: SimpleCompletionItem) => { diff --git a/code/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/code/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 701579d3626..b42fdfdaf3d 100644 --- a/code/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/code/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/code/src/vs/workbench/services/textfile/browser/textFileService.ts b/code/src/vs/workbench/services/textfile/browser/textFileService.ts index 231136ccb71..a0bdb7c5b74 100644 --- a/code/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/code/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -273,11 +273,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.fileService.writeFile(resource, readable, options); } - getEncoding(resource: URI): string { - const model = resource.scheme === Schemas.untitled ? this.untitled.get(resource) : this.files.get(resource); - return model?.getEncoding() ?? this.encoding.getUnvalidatedEncodingForResource(resource); - } - async getEncodedReadable(resource: URI | undefined, value: ITextSnapshot): Promise; async getEncodedReadable(resource: URI | undefined, value: string): Promise; async getEncodedReadable(resource: URI | undefined, value?: ITextSnapshot): Promise; @@ -317,14 +312,37 @@ export abstract class AbstractTextFileService extends Disposable implements ITex candidateGuessEncodings: options?.candidateGuessEncodings || this.textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings'), - overwriteEncoding: async detectedEncoding => { - const { encoding } = await this.encoding.getPreferredReadEncoding(resource, options, detectedEncoding ?? undefined); - - return encoding; - } + overwriteEncoding: async detectedEncoding => this.validateDetectedEncoding(resource, detectedEncoding ?? undefined, options) }); } + getEncoding(resource: URI): string { + const model = resource.scheme === Schemas.untitled ? this.untitled.get(resource) : this.files.get(resource); + return model?.getEncoding() ?? this.encoding.getUnvalidatedEncodingForResource(resource); + } + + async resolveDecoding(resource: URI | undefined, options?: IReadTextFileEncodingOptions): Promise<{ preferredEncoding: string; guessEncoding: boolean; candidateGuessEncodings: string[] }> { + return { + preferredEncoding: (await this.encoding.getPreferredReadEncoding(resource, options, undefined)).encoding, + guessEncoding: + options?.autoGuessEncoding || + this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + candidateGuessEncodings: + options?.candidateGuessEncodings || + this.textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings'), + }; + } + + async validateDetectedEncoding(resource: URI | undefined, detectedEncoding: string | undefined, options?: IReadTextFileEncodingOptions): Promise { + const { encoding } = await this.encoding.getPreferredReadEncoding(resource, options, detectedEncoding); + + return encoding; + } + + resolveEncoding(resource: URI | undefined, options?: IWriteTextFileOptions): Promise<{ encoding: string; addBOM: boolean }> { + return this.encoding.getWriteEncoding(resource, options); + } + //#endregion diff --git a/code/src/vs/workbench/services/textfile/common/textfiles.ts b/code/src/vs/workbench/services/textfile/common/textfiles.ts index efbd5451f99..84c90a5581b 100644 --- a/code/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/code/src/vs/workbench/services/textfile/common/textfiles.ts @@ -98,12 +98,6 @@ export interface ITextFileService extends IDisposable { */ create(operations: { resource: URI; value?: string | ITextSnapshot; options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo): Promise; - /** - * Get the encoding for the provided `resource`. Will try to determine the encoding - * from any existing model for that `resource` and fallback to the configured defaults. - */ - getEncoding(resource: URI): string; - /** * Returns the readable that uses the appropriate encoding. This method should * be used whenever a `string` or `ITextSnapshot` is being persisted to the @@ -122,6 +116,27 @@ export interface ITextFileService extends IDisposable { * Will throw an error if `acceptTextOnly: true` for resources that seem to be binary. */ getDecodedStream(resource: URI | undefined, value: VSBufferReadableStream, options?: IReadTextFileEncodingOptions): Promise>; + + /** + * Get the encoding for the provided `resource`. Will try to determine the encoding + * from any existing model for that `resource` and fallback to the configured defaults. + */ + getEncoding(resource: URI): string; + + /** + * Get the properties for decoding the provided `resource` based on configuration. + */ + resolveDecoding(resource: URI | undefined, options?: IReadTextFileEncodingOptions): Promise<{ preferredEncoding: string; guessEncoding: boolean; candidateGuessEncodings: string[] }>; + + /** + * Get the properties for encoding the provided `resource` based on configuration. + */ + resolveEncoding(resource: URI | undefined, options?: IWriteTextFileOptions): Promise<{ encoding: string; addBOM: boolean }>; + + /** + * Given a detected encoding, validate it against the configured encoding options. + */ + validateDetectedEncoding(resource: URI | undefined, detectedEncoding: string, options?: IReadTextFileEncodingOptions): Promise; } export interface IReadTextFileEncodingOptions { diff --git a/code/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/code/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 56a3b2d8f18..d85b23015f4 100644 --- a/code/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/code/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -16,7 +16,7 @@ import { ColorThemeData } from '../common/colorThemeData.js'; import { IColorTheme, Extensions as ThemingExtensions, IThemingRegistry } from '../../../../platform/theme/common/themeService.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { registerFileIconThemeSchemas } from '../common/fileIconThemeSchema.js'; -import { IDisposable, dispose, Disposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { FileIconThemeData, FileIconThemeLoader } from './fileIconThemeData.js'; import { createStyleSheet } from '../../../../base/browser/domStylesheets.js'; import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; @@ -761,30 +761,33 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme class ThemeFileWatcher { private watchedLocation: URI | undefined; - private watcherDisposable: IDisposable | undefined; - private fileChangeListener: IDisposable | undefined; + private readonly watcherDisposables = new DisposableStore(); - constructor(private fileService: IFileService, private environmentService: IBrowserWorkbenchEnvironmentService, private onUpdate: () => void) { - } + constructor( + private readonly fileService: IFileService, + private readonly environmentService: IBrowserWorkbenchEnvironmentService, + private readonly onUpdate: () => void + ) { } update(theme: { location?: URI; watch?: boolean }) { if (!resources.isEqual(theme.location, this.watchedLocation)) { - this.dispose(); + this.watchedLocation = undefined; + this.watcherDisposables.clear(); + if (theme.location && (theme.watch || this.environmentService.isExtensionDevelopment)) { this.watchedLocation = theme.location; - this.watcherDisposable = this.fileService.watch(theme.location); - this.fileService.onDidFilesChange(e => { + this.watcherDisposables.add(this.fileService.watch(theme.location)); + this.watcherDisposables.add(this.fileService.onDidFilesChange(e => { if (this.watchedLocation && e.contains(this.watchedLocation, FileChangeType.UPDATED)) { this.onUpdate(); } - }); + })); } } } dispose() { - this.watcherDisposable = dispose(this.watcherDisposable); - this.fileChangeListener = dispose(this.fileChangeListener); + this.watcherDisposables.dispose(); this.watchedLocation = undefined; } } diff --git a/code/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/code/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index f42e6c2081b..06dd1568acf 100644 --- a/code/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/code/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -761,7 +761,7 @@ export class TreeSitterTokenizationSupport extends Disposable implements ITreeSi } for (let i = 0; i < endOffsetsAndScopes.length; i++) { const token = endOffsetsAndScopes[i]; - if (token.endOffset === 0 && token.scopes.length === 0 && i !== 0) { + if (token.endOffset === 0 && i !== 0) { endOffsetsAndScopes.splice(i, endOffsetsAndScopes.length - i); break; } diff --git a/code/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts b/code/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts index 7b29090f272..547a1690868 100644 --- a/code/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts +++ b/code/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/code/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/code/src/vs/workbench/services/userDataSync/common/userDataSync.ts index cca96a1da14..2739b6c195f 100644 --- a/code/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/code/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -58,7 +58,7 @@ export function getSyncAreaLabel(source: SyncResource): string { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); case SyncResource.Snippets: return localize('snippets', "Snippets"); - case SyncResource.Prompts: return localize('prompts', "Prompts"); + case SyncResource.Prompts: return localize('prompts', "Prompts and Instructions"); case SyncResource.Tasks: return localize('tasks', "Tasks"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); diff --git a/code/src/vs/workbench/services/views/browser/viewsService.ts b/code/src/vs/workbench/services/views/browser/viewsService.ts index a852be8ed7b..9e4053c2535 100644 --- a/code/src/vs/workbench/services/views/browser/viewsService.ts +++ b/code/src/vs/workbench/services/views/browser/viewsService.ts @@ -471,68 +471,84 @@ export class ViewsService extends Disposable implements IViewsService { private registerOpenViewAction(viewDescriptor: IViewDescriptor): IDisposable { const disposables = new DisposableStore(); - if (viewDescriptor.openCommandActionDescriptor) { - const title = viewDescriptor.openCommandActionDescriptor.title ?? viewDescriptor.name; - const commandId = viewDescriptor.openCommandActionDescriptor.id; - const that = this; - disposables.add(registerAction2(class OpenViewAction extends Action2 { - constructor() { - super({ - id: commandId, - get title(): ICommandActionTitle { - const viewContainerLocation = that.viewDescriptorService.getViewLocationById(viewDescriptor.id); - const localizedTitle = typeof title === 'string' ? title : title.value; - const originalTitle = typeof title === 'string' ? title : title.original; - if (viewContainerLocation === ViewContainerLocation.Sidebar) { - return { value: localize('show view', "Show {0}", localizedTitle), original: `Show ${originalTitle}` }; - } else { - return { value: localize('toggle view', "Toggle {0}", localizedTitle), original: `Toggle ${originalTitle}` }; - } - }, - category: Categories.View, - precondition: ContextKeyExpr.has(`${viewDescriptor.id}.active`), - keybinding: viewDescriptor.openCommandActionDescriptor!.keybindings ? { ...viewDescriptor.openCommandActionDescriptor!.keybindings, weight: KeybindingWeight.WorkbenchContrib } : undefined, - f1: true - }); - } - public async run(serviceAccessor: ServicesAccessor): Promise { - const editorGroupService = serviceAccessor.get(IEditorGroupsService); - const viewDescriptorService = serviceAccessor.get(IViewDescriptorService); - const layoutService = serviceAccessor.get(IWorkbenchLayoutService); - const viewsService = serviceAccessor.get(IViewsService); - const contextKeyService = serviceAccessor.get(IContextKeyService); - - const focusedViewId = FocusedViewContext.getValue(contextKeyService); - if (focusedViewId === viewDescriptor.id) { - - const viewLocation = viewDescriptorService.getViewLocationById(viewDescriptor.id); - if (viewDescriptorService.getViewLocationById(viewDescriptor.id) === ViewContainerLocation.Sidebar) { - // focus the editor if the view is focused and in the side bar - editorGroupService.activeGroup.focus(); - } else if (viewLocation !== null) { - // otherwise hide the part where the view lives if focused - layoutService.setPartHidden(true, getPartByLocation(viewLocation)); + const title = viewDescriptor.openCommandActionDescriptor?.title ?? viewDescriptor.name; + const commandId = viewDescriptor.openCommandActionDescriptor?.id ?? `${viewDescriptor.id}.open`; + const that = this; + disposables.add(registerAction2(class OpenViewAction extends Action2 { + constructor() { + super({ + id: commandId, + get title(): ICommandActionTitle { + const viewContainerLocation = that.viewDescriptorService.getViewLocationById(viewDescriptor.id); + const localizedTitle = typeof title === 'string' ? title : title.value; + const originalTitle = typeof title === 'string' ? title : title.original; + if (viewContainerLocation === ViewContainerLocation.Sidebar) { + return { value: localize('show view', "Show {0}", localizedTitle), original: `Show ${originalTitle}` }; + } else { + return { value: localize('toggle view', "Toggle {0}", localizedTitle), original: `Toggle ${originalTitle}` }; } - } else { - viewsService.openView(viewDescriptor.id, true); + }, + category: Categories.View, + precondition: ContextKeyExpr.has(`${viewDescriptor.id}.active`), + keybinding: viewDescriptor.openCommandActionDescriptor?.keybindings ? { ...viewDescriptor.openCommandActionDescriptor.keybindings, weight: KeybindingWeight.WorkbenchContrib } : undefined, + f1: viewDescriptor.openCommandActionDescriptor ? true : undefined, + metadata: { + description: localize('open view', "Opens view {0}", viewDescriptor.name.value), + args: [ + { + name: 'options', + schema: { + type: 'object', + properties: { + 'preserveFocus': { + type: 'boolean', + default: false, + description: localize('preserveFocus', "Whether to preserve the existing focus when opening the view.") + } + }, + } + } + ] + } + }); + } + public async run(serviceAccessor: ServicesAccessor, options?: { preserveFocus?: boolean }): Promise { + const editorGroupService = serviceAccessor.get(IEditorGroupsService); + const viewDescriptorService = serviceAccessor.get(IViewDescriptorService); + const layoutService = serviceAccessor.get(IWorkbenchLayoutService); + const viewsService = serviceAccessor.get(IViewsService); + const contextKeyService = serviceAccessor.get(IContextKeyService); + + const focusedViewId = FocusedViewContext.getValue(contextKeyService); + if (focusedViewId === viewDescriptor.id && !options?.preserveFocus) { + + const viewLocation = viewDescriptorService.getViewLocationById(viewDescriptor.id); + if (viewDescriptorService.getViewLocationById(viewDescriptor.id) === ViewContainerLocation.Sidebar) { + // focus the editor if the view is focused and in the side bar + editorGroupService.activeGroup.focus(); + } else if (viewLocation !== null) { + // otherwise hide the part where the view lives if focused + layoutService.setPartHidden(true, getPartByLocation(viewLocation)); } + } else { + viewsService.openView(viewDescriptor.id, !options?.preserveFocus); } - })); + } + })); - if (viewDescriptor.openCommandActionDescriptor.mnemonicTitle) { - const defaultViewContainer = this.viewDescriptorService.getDefaultContainerById(viewDescriptor.id); - if (defaultViewContainer) { - const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(defaultViewContainer); - disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - command: { - id: commandId, - title: viewDescriptor.openCommandActionDescriptor.mnemonicTitle, - }, - group: defaultLocation === ViewContainerLocation.Sidebar ? '3_sidebar' : defaultLocation === ViewContainerLocation.AuxiliaryBar ? '4_auxbar' : '5_panel', - when: ContextKeyExpr.has(`${viewDescriptor.id}.active`), - order: viewDescriptor.openCommandActionDescriptor.order ?? Number.MAX_VALUE - })); - } + if (viewDescriptor.openCommandActionDescriptor?.mnemonicTitle) { + const defaultViewContainer = this.viewDescriptorService.getDefaultContainerById(viewDescriptor.id); + if (defaultViewContainer) { + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(defaultViewContainer); + disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + command: { + id: commandId, + title: viewDescriptor.openCommandActionDescriptor.mnemonicTitle, + }, + group: defaultLocation === ViewContainerLocation.Sidebar ? '3_sidebar' : defaultLocation === ViewContainerLocation.AuxiliaryBar ? '4_auxbar' : '5_panel', + when: ContextKeyExpr.has(`${viewDescriptor.id}.active`), + order: viewDescriptor.openCommandActionDescriptor.order ?? Number.MAX_VALUE + })); } } return disposables; diff --git a/code/src/vs/workbench/services/workspaces/common/workspaceUtils.ts b/code/src/vs/workbench/services/workspaces/common/workspaceUtils.ts index 4ba2f5af24a..a67b3457ad0 100644 --- a/code/src/vs/workbench/services/workspaces/common/workspaceUtils.ts +++ b/code/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/code/src/vs/workbench/test/browser/workbenchTestServices.ts b/code/src/vs/workbench/test/browser/workbenchTestServices.ts index 891747556c9..293bec7e685 100644 --- a/code/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/code/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -32,7 +32,7 @@ import { ILanguageService } from '../../../editor/common/languages/language.js'; import { IHistoryService } from '../../services/history/common/history.js'; import { IInstantiationService, ServiceIdentifier } from '../../../platform/instantiation/common/instantiation.js'; import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; -import { MenuBarVisibility, IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions } from '../../../platform/window/common/window.js'; +import { MenuBarVisibility, IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IRectangle } from '../../../platform/window/common/window.js'; import { TestWorkspace } from '../../../platform/workspace/test/common/testWorkspace.js'; import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; @@ -183,6 +183,7 @@ import { IHoverService } from '../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../platform/hover/test/browser/nullHoverService.js'; import { IActionViewItemService, NullActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IElementData } from '../../../platform/native/common/native.js'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -966,7 +967,7 @@ export class TestEditorGroupView implements IEditorGroupView { copyEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): void { } async closeEditor(_editor?: EditorInput, options?: ICloseEditorOptions): Promise { return true; } async closeEditors(_editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise { return true; } - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise { return true; } + closeAllEditors(options?: ICloseAllEditorsOptions): any { return true; } async replaceEditors(_editors: IEditorReplacement[]): Promise { } pinEditor(_editor?: EditorInput): void { } stickEditor(editor?: EditorInput | undefined): void { } @@ -1577,7 +1578,8 @@ export class TestHostService implements IHostService { async toggleFullScreen(): Promise { } - async getScreenshot(): Promise { return undefined; } + async getScreenshot(rect?: IRectangle): Promise { return undefined; } + async getElementData(rect: IRectangle, token: CancellationToken,): Promise { return undefined; } async getNativeWindowHandle(_windowId: number): Promise { return undefined; } @@ -2205,6 +2207,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens onProfileAwareDidUpdateExtensionMetadata = Event.None; onDidChangeProfile = Event.None; onDidEnableExtensions = Event.None; + preferPreReleases = true; installVSIX(location: URI, manifest: Readonly, installOptions?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); } @@ -2247,7 +2250,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens throw new Error('Method not implemented.'); } copyExtensions(): Promise { throw new Error('Not Supported'); } - toggleAppliationScope(): Promise { throw new Error('Not Supported'); } + toggleApplicationScope(): Promise { throw new Error('Not Supported'); } installExtensionsFromProfile(): Promise { throw new Error('Not Supported'); } whenProfileChanged(from: IUserDataProfile, to: IUserDataProfile): Promise { throw new Error('Not Supported'); } getInstalledWorkspaceExtensionLocations(): URI[] { throw new Error('Method not implemented.'); } diff --git a/code/src/vs/workbench/test/common/notifications.test.ts b/code/src/vs/workbench/test/common/notifications.test.ts index 510a48dc03c..80cb08bf05f 100644 --- a/code/src/vs/workbench/test/common/notifications.test.ts +++ b/code/src/vs/workbench/test/common/notifications.test.ts @@ -275,7 +275,7 @@ suite('Notifications', () => { assert.strictEqual(model.statusMessage!.message, 'Hello World'); assert.strictEqual(lastStatusMessageEvent.item.message, model.statusMessage!.message); assert.strictEqual(lastStatusMessageEvent.kind, StatusMessageChangeType.ADD); - disposable.dispose(); + disposable.close(); assert.ok(!model.statusMessage); assert.strictEqual(lastStatusMessageEvent.kind, StatusMessageChangeType.REMOVE); @@ -284,10 +284,10 @@ suite('Notifications', () => { assert.strictEqual(model.statusMessage!.message, 'Hello World 3'); - disposable2.dispose(); + disposable2.close(); assert.strictEqual(model.statusMessage!.message, 'Hello World 3'); - disposable3.dispose(); + disposable3.close(); assert.ok(!model.statusMessage); item2DuplicateHandle.close(); diff --git a/code/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/code/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 9bc958141ff..4358257f5aa 100644 --- a/code/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/code/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -6,7 +6,7 @@ import { Event } from '../../../base/common/event.js'; import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestEncodingOracle, TestEnvironmentService, TestFileDialogService, TestFilesConfigurationService, TestFileService, TestLifecycleService, TestTextFileService } from '../browser/workbenchTestServices.js'; import { ISharedProcessService } from '../../../platform/ipc/electron-sandbox/services.js'; -import { INativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../../../platform/native/common/native.js'; +import { INativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IElementData } from '../../../platform/native/common/native.js'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; @@ -77,6 +77,7 @@ export class TestNativeHostService implements INativeHostService { onDidChangePassword = Event.None; onDidTriggerWindowSystemContextMenu: Event<{ windowId: number; x: number; y: number }> = Event.None; onDidChangeWindowFullScreen = Event.None; + onDidChangeWindowAlwaysOnTop = Event.None; onDidChangeDisplay = Event.None; windowCount = Promise.resolve(1); @@ -100,6 +101,9 @@ export class TestNativeHostService implements INativeHostService { async unmaximizeWindow(): Promise { } async minimizeWindow(): Promise { } async moveWindowTop(options?: INativeHostOptions): Promise { } + async isWindowAlwaysOnTop(options?: INativeHostOptions): Promise { return false; } + async toggleWindowAlwaysOnTop(options?: INativeHostOptions): Promise { } + async setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise { } getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); } async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise { } async updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise { } @@ -156,12 +160,14 @@ export class TestNativeHostService implements INativeHostService { async readClipboardFindText(): Promise { return ''; } async writeClipboardFindText(text: string): Promise { } async writeClipboardBuffer(format: string, buffer: VSBuffer, type?: 'selection' | 'clipboard' | undefined): Promise { } + async triggerPaste(options?: INativeHostOptions): Promise { } async readImage(): Promise { return Uint8Array.from([]); } async readClipboardBuffer(format: string): Promise { return VSBuffer.wrap(Uint8Array.from([])); } 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(rect?: IRectangle): Promise { return undefined; } + async getElementData(rect: IRectangle, token: CancellationToken, cancellationId?: number): Promise { return undefined; } } export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { diff --git a/code/src/vs/workbench/workbench.common.main.ts b/code/src/vs/workbench/workbench.common.main.ts index 248d6b07c73..93e4c4fc944 100644 --- a/code/src/vs/workbench/workbench.common.main.ts +++ b/code/src/vs/workbench/workbench.common.main.ts @@ -69,6 +69,7 @@ import './services/editor/browser/editorService.js'; import './services/editor/browser/editorResolverService.js'; import './services/aiEmbeddingVector/common/aiEmbeddingVectorService.js'; import './services/aiRelatedInformation/common/aiRelatedInformationService.js'; +import './services/aiSettingsSearch/common/aiSettingsSearchService.js'; import './services/history/browser/historyService.js'; import './services/activity/browser/activityService.js'; import './services/keybinding/browser/keybindingService.js'; @@ -147,6 +148,7 @@ import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService import { ExtensionStorageService, IExtensionStorageService } from '../platform/extensionManagement/common/extensionStorage.js'; import { IUserDataSyncLogService } from '../platform/userDataSync/common/userDataSync.js'; import { UserDataSyncLogService } from '../platform/userDataSync/common/userDataSyncLog.js'; +import { AllowedExtensionsService } from '../platform/extensionManagement/common/allowedExtensionsService.js'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed); registerSingleton(IAllowedExtensionsService, AllowedExtensionsService, InstantiationType.Delayed); @@ -378,9 +380,6 @@ import './contrib/list/browser/list.contribution.js'; // Accessibility Signals import './contrib/accessibilitySignals/browser/accessibilitySignal.contribution.js'; -// Deprecated Extension Migrator -import './contrib/deprecatedExtensionMigrator/browser/deprecatedExtensionMigrator.contribution.js'; - // Bracket Pair Colorizer 2 Telemetry import './contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution.js'; @@ -398,7 +397,6 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; // Drop or paste into import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; -import { AllowedExtensionsService } from '../platform/extensionManagement/common/allowedExtensionsService.js'; //#endregion diff --git a/code/src/vs/workbench/workbench.web.main.internal.ts b/code/src/vs/workbench/workbench.web.main.internal.ts index 59a19ef5c0a..ca91b124d63 100644 --- a/code/src/vs/workbench/workbench.web.main.internal.ts +++ b/code/src/vs/workbench/workbench.web.main.internal.ts @@ -139,9 +139,6 @@ import './contrib/debug/browser/extensionHostDebugService.js'; // Welcome Banner import './contrib/welcomeBanner/browser/welcomeBanner.contribution.js'; -// Welcome Dialog -import './contrib/welcomeDialog/browser/welcomeDialog.contribution.js'; - // Webview import './contrib/webview/browser/webview.web.contribution.js'; diff --git a/code/src/vscode-dts/vscode.d.ts b/code/src/vscode-dts/vscode.d.ts index b206caf2d71..cefcb1a81b3 100644 --- a/code/src/vscode-dts/vscode.d.ts +++ b/code/src/vscode-dts/vscode.d.ts @@ -116,6 +116,24 @@ declare module 'vscode' { */ readonly languageId: string; + /** + * The file encoding of this document that will be used when the document is saved. + * + * Use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event to + * get notified when the document encoding changes. + * + * Note that the possible encoding values are currently defined as any of the following: + * 'utf8', 'utf8bom', 'utf16le', 'utf16be', 'windows1252', 'iso88591', 'iso88593', + * 'iso885915', 'macroman', 'cp437', 'windows1256', 'iso88596', 'windows1257', + * 'iso88594', 'iso885914', 'windows1250', 'iso88592', 'cp852', 'windows1251', + * 'cp866', 'cp1125', 'iso88595', 'koi8r', 'koi8u', 'iso885913', 'windows1253', + * 'iso88597', 'windows1255', 'iso88598', 'iso885910', 'iso885916', 'windows1254', + * 'iso88599', 'windows1258', 'gbk', 'gb18030', 'cp950', 'big5hkscs', 'shiftjis', + * 'eucjp', 'euckr', 'windows874', 'iso885911', 'koi8ru', 'koi8t', 'gb2312', + * 'cp865', 'cp850'. + */ + readonly encoding: string; + /** * The version number of this document (it will strictly increase after each * change, including undo/redo). @@ -11997,6 +12015,8 @@ declare module 'vscode' { * When the user starts dragging items from this `DragAndDropController`, `handleDrag` will be called. * Extensions can use `handleDrag` to add their {@link DataTransferItem `DataTransferItem`} items to the drag and drop. * + * Mime types added in `handleDrag` won't be available outside the application. + * * When the items are dropped on **another tree item** in **the same tree**, your `DataTransferItem` objects * will be preserved. Use the recommended mime type for the tree (`application/vnd.code.tree.`) to add * tree objects in a data transfer. See the documentation for `DataTransferItem` for how best to take advantage of this. @@ -13990,7 +14010,29 @@ declare module 'vscode' { * @param uri Identifies the resource to open. * @returns A promise that resolves to a {@link TextDocument document}. */ - export function openTextDocument(uri: Uri): Thenable; + export function openTextDocument(uri: Uri, options?: { + /** + * The {@link TextDocument.encoding encoding} of the document to use + * for decoding the underlying buffer to text. If omitted, the encoding + * will be guessed based on the file content and/or the editor settings + * unless the document is already opened. + * + * Opening a text document that was already opened with a different encoding + * has the potential of changing the text contents of the text document. + * Specifically, when the encoding results in a different set of characters + * than the previous encoding. As such, an error is thrown for dirty documents + * when the specified encoding is different from the encoding of the document. + * + * See {@link TextDocument.encoding} for more information about valid + * values for encoding. Using an unsupported encoding will fallback to the + * default encoding for the document. + * + * *Note* that if you open a document with an encoding that does not + * support decoding the underlying bytes, content may be replaced with + * substitution characters as appropriate. + */ + readonly encoding?: string; + }): Thenable; /** * A short-hand for `openTextDocument(Uri.file(path))`. @@ -13999,7 +14041,29 @@ declare module 'vscode' { * @param path A path of a file on disk. * @returns A promise that resolves to a {@link TextDocument document}. */ - export function openTextDocument(path: string): Thenable; + export function openTextDocument(path: string, options?: { + /** + * The {@link TextDocument.encoding encoding} of the document to use + * for decoding the underlying buffer to text. If omitted, the encoding + * will be guessed based on the file content and/or the editor settings + * unless the document is already opened. + * + * Opening a text document that was already opened with a different encoding + * has the potential of changing the text contents of the text document. + * Specifically, when the encoding results in a different set of characters + * than the previous encoding. As such, an error is thrown for dirty documents + * when the specified encoding is different from the encoding of the document. + * + * See {@link TextDocument.encoding} for more information about valid + * values for encoding. Using an unsupported encoding will fallback to the + * default encoding for the document. + * + * *Note* that if you open a document with an encoding that does not + * support decoding the underlying bytes, content may be replaced with + * substitution characters as appropriate. + */ + readonly encoding?: string; + }): Thenable; /** * Opens an untitled text document. The editor will prompt the user for a file @@ -14018,6 +14082,14 @@ declare module 'vscode' { * The initial contents of the document. */ content?: string; + /** + * The {@link TextDocument.encoding encoding} of the document. + * + * See {@link TextDocument.encoding} for more information about valid + * values for encoding. Using an unsupported encoding will fallback to the + * default encoding for the document. + */ + readonly encoding?: string; }): Thenable; /** @@ -14301,6 +14373,129 @@ declare module 'vscode' { * Event that fires when the current workspace has been trusted. */ export const onDidGrantWorkspaceTrust: Event; + + /** + * Decodes the content from a `Uint8Array` to a `string`. You MUST + * provide the entire content at once to ensure that the encoding + * can properly apply. Do not use this method to decode content + * in chunks, as that may lead to incorrect results. + * + * Will pick an encoding based on settings and the content of the + * buffer (for example byte order marks). + * + * *Note* that if you decode content that is unsupported by the + * encoding, the result may contain substitution characters as + * appropriate. + * + * @throws This method will throw an error when the content is binary. + * + * @param content The text content to decode as a `Uint8Array`. + * @returns A thenable that resolves to the decoded `string`. + */ + export function decode(content: Uint8Array): Thenable; + + /** + * Decodes the content from a `Uint8Array` to a `string` using the + * provided encoding. You MUST provide the entire content at once + * to ensure that the encoding can properly apply. Do not use this + * method to decode content in chunks, as that may lead to incorrect + * results. + * + * *Note* that if you decode content that is unsupported by the + * encoding, the result may contain substitution characters as + * appropriate. + * + * @throws This method will throw an error when the content is binary. + * + * @param content The text content to decode as a `Uint8Array`. + * @param options Additional context for picking the encoding. + * @returns A thenable that resolves to the decoded `string`. + */ + export function decode(content: Uint8Array, options: { + /** + * Allows to explicitly pick the encoding to use. + * See {@link TextDocument.encoding} for more information + * about valid values for encoding. + * Using an unsupported encoding will fallback to the + * default configured encoding. + */ + readonly encoding: string; + }): Thenable; + + /** + * Decodes the content from a `Uint8Array` to a `string`. You MUST + * provide the entire content at once to ensure that the encoding + * can properly apply. Do not use this method to decode content + * in chunks, as that may lead to incorrect results. + * + * The encoding is picked based on settings and the content + * of the buffer (for example byte order marks). + * + * *Note* that if you decode content that is unsupported by the + * encoding, the result may contain substitution characters as + * appropriate. + * + * @throws This method will throw an error when the content is binary. + * + * @param content The content to decode as a `Uint8Array`. + * @param options Additional context for picking the encoding. + * @returns A thenable that resolves to the decoded `string`. + */ + export function decode(content: Uint8Array, options: { + /** + * The URI that represents the file if known. This information + * is used to figure out the encoding related configuration + * for the file if any. + */ + readonly uri: Uri; + }): Thenable; + + /** + * Encodes the content of a `string` to a `Uint8Array`. + * + * Will pick an encoding based on settings. + * + * @param content The content to decode as a `string`. + * @returns A thenable that resolves to the encoded `Uint8Array`. + */ + export function encode(content: string): Thenable; + + /** + * Encodes the content of a `string` to a `Uint8Array` using the + * provided encoding. + * + * @param content The content to decode as a `string`. + * @param options Additional context for picking the encoding. + * @returns A thenable that resolves to the encoded `Uint8Array`. + */ + export function encode(content: string, options: { + /** + * Allows to explicitly pick the encoding to use. + * See {@link TextDocument.encoding} for more information + * about valid values for encoding. + * Using an unsupported encoding will fallback to the + * default configured encoding. + */ + readonly encoding: string; + }): Thenable; + + /** + * Encodes the content of a `string` to a `Uint8Array`. + * + * The encoding is picked based on settings. + * + * @param content The content to decode as a `string`. + * @param options Additional context for picking the encoding. + * @returns A thenable that resolves to the encoded `Uint8Array`. + */ + export function encode(content: string, options: { + /** + * The URI that represents the file if known. This information + * is used to figure out the encoding related configuration + * for the file if any. + */ + readonly uri: Uri; + }): Thenable; } /** @@ -16295,7 +16490,7 @@ declare module 'vscode' { } /** - * Namespace for source control mangement. + * Namespace for source control management. */ export namespace scm { @@ -16609,7 +16804,7 @@ declare module 'vscode' { export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; /** - * A debug adaper factory that creates {@link DebugAdapterDescriptor debug adapter descriptors}. + * A debug adapter factory that creates {@link DebugAdapterDescriptor debug adapter descriptors}. */ export interface DebugAdapterDescriptorFactory { /** @@ -16663,7 +16858,7 @@ declare module 'vscode' { } /** - * A debug adaper factory that creates {@link DebugAdapterTracker debug adapter trackers}. + * A debug adapter factory that creates {@link DebugAdapterTracker debug adapter trackers}. */ export interface DebugAdapterTrackerFactory { /** @@ -17200,7 +17395,7 @@ declare module 'vscode' { * Whether the thread supports reply. * Defaults to true. */ - canReply: boolean; + canReply: boolean | CommentAuthorInformation; /** * Context value of the comment thread. This can be used to contribute thread specific actions. @@ -19062,7 +19257,7 @@ declare module 'vscode' { readonly value: T; /** - * Creates a new telementry trusted value. + * Creates a new telemetry trusted value. * * @param value A value to trust */ @@ -19070,7 +19265,7 @@ declare module 'vscode' { } /** - * A telemetry logger which can be used by extensions to log usage and error telementry. + * A telemetry logger which can be used by extensions to log usage and error telemetry. * * A logger wraps around an {@link TelemetrySender sender} but it guarantees that * - user settings to disable or tweak telemetry are respected, and that @@ -20249,7 +20444,7 @@ declare module 'vscode' { */ export class LanguageModelToolResult { /** - * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in + * A list of tool result content parts. Includes `unknown` because this list may be extended with new content types in * the future. * @see {@link lm.invokeTool}. */ diff --git a/code/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts b/code/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts new file mode 100644 index 00000000000..373c5a42ae0 --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3 + } + + export interface SettingsSearchResult { + query: string; + kind: SettingsSearchResultKind; + settings: string[]; + } + + export interface SettingsSearchProviderOptions { + limit: number; + } + + export interface SettingsSearchProvider { + provideSettingsSearchResults(query: string, option: SettingsSearchProviderOptions, progress: Progress, token: CancellationToken): Thenable; + } + + export namespace ai { + export function registerSettingsSearchProvider(provider: SettingsSearchProvider): Disposable; + } +} diff --git a/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 2a33fc222f9..68be8e580b6 100644 --- a/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -79,7 +79,7 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart; export class ChatResponseWarningPart { value: MarkdownString; @@ -159,6 +159,13 @@ declare module 'vscode' { resolve?(token: CancellationToken): Thenable; } + export class ChatResponseExtensionsPart { + + readonly extensions: string[]; + + constructor(extensions: string[]); + } + export interface ChatResponseStream { /** @@ -220,19 +227,6 @@ declare module 'vscode' { } - export interface ChatRequest { - - /** - * A list of tools that the user selected for this request, when `undefined` any tool - * from {@link lm.tools} should be used. - * - * Tools can be called with {@link lm.invokeTool} with input that match their - * declared `inputSchema`. - */ - readonly tools: readonly LanguageModelToolInformation[] | undefined; - } - - /** * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? * Does it show up in history? @@ -249,6 +243,14 @@ declare module 'vscode' { rejectedConfirmationData?: any[]; } + export interface ChatRequest { + + /** + * A map of all tools that should (`true`) and should not (`false`) be used in this request. + */ + readonly tools: Map; + } + // TODO@API fit this into the stream export interface ChatUsedContext { documents: ChatDocumentContext[]; @@ -262,7 +264,7 @@ declare module 'vscode' { /** * Event that fires when a request is paused or unpaused. - * Chat requests are initialy unpaused in the {@link requestHandler}. + * Chat requests are initially unpaused in the {@link requestHandler}. */ onDidChangePauseState: Event; } @@ -405,7 +407,7 @@ declare module 'vscode' { } export namespace lm { - export function fileIsIgnored(uri: Uri, token: CancellationToken): Thenable; + export function fileIsIgnored(uri: Uri, token?: CancellationToken): Thenable; } export interface ChatVariableValue { diff --git a/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 7b3304b70fb..8b0d17fa0ca 100644 --- a/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 6 +// version: 9 declare module 'vscode' { @@ -27,10 +27,6 @@ declare module 'vscode' { * Code editor inline chat */ Editor = 4, - /** - * Chat is happening in an editing session - */ - EditingSession = 5, } export class ChatRequestEditorData { @@ -81,6 +77,67 @@ declare module 'vscode' { * or terminal. Will be `undefined` for the chat panel. */ readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; + + /** + * Events for edited files in this session collected since the last request. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + } + + export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, + } + + export interface ChatRequestEditedFileEvent { + readonly uri: Uri; + readonly eventKind: ChatRequestEditedFileEventKind; + } + + /** + * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. + */ + export class ChatRequestTurn2 { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * The list of tools were attached to this request. + */ + readonly toolReferences: readonly ChatLanguageModelToolReference[]; + + /** + * Events for edited files in this session collected between the previous request and this one. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); } export interface ChatParticipant { @@ -183,4 +240,26 @@ declare module 'vscode' { } // #endregion + + export interface ChatRequestToolSelection { + /** + * A list of tools that the user selected for this request. + * Tools can be called with {@link lm.invokeTool} with input that match their + * declared `inputSchema`. + */ + readonly tools: readonly LanguageModelToolInformation[]; + + /** + * When true, only this set of tools (and toolReferences) should be used. When false, the base set of agent tools can also be included. + */ + readonly isExclusive?: boolean; + } + + export interface ChatRequest { + /** + * A list of tools that the user selected for this request, when `undefined` any tool + * from {@link lm.tools} should be used. + */ + readonly toolSelection: ChatRequestToolSelection | undefined; + } } diff --git a/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts index dd3d55992db..a47bc76133f 100644 --- a/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -5,11 +5,6 @@ declare module 'vscode' { - export interface ChatResponseFragment { - index: number; - part: string; - } - export interface ChatResponseFragment2 { index: number; part: LanguageModelTextPart | LanguageModelToolCallPart; @@ -26,9 +21,6 @@ declare module 'vscode' { provideLanguageModelResponse(messages: Array, options: LanguageModelChatRequestOptions, extensionId: string, progress: Progress, token: CancellationToken): Thenable; - /** @deprecated */ - provideLanguageModelResponse2?(messages: Array, options: LanguageModelChatRequestOptions, extensionId: string, progress: Progress, token: CancellationToken): Thenable; - provideTokenCount(text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Thenable; } @@ -48,6 +40,16 @@ declare module 'vscode' { */ readonly family: string; + /** + * An optional, human-readable description of the language model. + */ + readonly description?: string; + + /** + * An optional, human-readable string representing the cost of using the language model. + */ + readonly cost?: string; + /** * Opaque version string of the model. This is defined by the extension contributing the language model * and subject to change while the identifier is stable. @@ -73,6 +75,14 @@ declare module 'vscode' { readonly toolCalling?: boolean; readonly agentMode?: boolean; }; + + /** + * Optional category to group models by in the model picker. + * The lower the order, the higher the category appears in the list. + * Has no effect if `isUserSelectable` is `false`. + * If not specified, the model will appear in the "Other Models" category. + */ + readonly category?: { label: string; order: number }; } export interface ChatResponseProviderMetadata { @@ -80,14 +90,6 @@ declare module 'vscode' { extensions?: string[]; } - export namespace chat { - - /** - * @deprecated use `lm.registerChatResponseProvider` instead - */ - export function registerChatResponseProvider(id: string, provider: ChatResponseProvider, metadata: ChatResponseProviderMetadata): Disposable; - } - export namespace lm { export function registerChatModelProvider(id: string, provider: LanguageModelChatProvider, metadata: ChatResponseProviderMetadata): Disposable; diff --git a/code/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts b/code/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts index 28ba4157291..b03afe16ca1 100644 --- a/code/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts +++ b/code/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts @@ -14,7 +14,7 @@ declare module 'vscode' { /** * The main name of the entry, like 'Indexing Status' */ - title: string; + title: string | { label: string; link: string }; /** * Optional additional description of the entry. diff --git a/code/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/code/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts index fb99abb48bd..772771eef77 100644 --- a/code/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts +++ b/code/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -32,7 +32,7 @@ declare module 'vscode' { range: Range | undefined; comments: readonly Comment[]; collapsibleState: CommentThreadCollapsibleState; - canReply: boolean; + canReply: boolean | CommentAuthorInformation; contextValue?: string; label?: string; dispose(): void; diff --git a/code/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts b/code/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts index 9f866269987..c28b6b4cc42 100644 --- a/code/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts +++ b/code/src/vscode-dts/vscode.proposed.contribDebugCreateConfiguration.d.ts @@ -3,4 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// empty placeholder declaration for the `debugCreateConfiguation` menu +// empty placeholder declaration for the `debugCreateConfiguration` menu diff --git a/code/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/code/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index cd2a093a4a8..830ae96509a 100644 --- a/code/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/code/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 3 +// version: 4 declare module 'vscode' { @@ -50,7 +50,7 @@ declare module 'vscode' { helpTextPostfix?: string | MarkdownString; welcomeMessageProvider?: ChatWelcomeMessageProvider; - welcomeMessageContent?: ChatWelcomeMessageContent; + additionalWelcomeMessage?: string | MarkdownString; titleProvider?: ChatTitleProvider; requester?: ChatRequesterInformation; } diff --git a/code/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/code/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index d7c85101091..34eef9c6dab 100644 --- a/code/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/code/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -44,6 +44,13 @@ declare module 'vscode' { showInlineEditMenu?: boolean; action?: Command; + + displayLocation?: InlineCompletionDisplayLocation; + } + + export interface InlineCompletionDisplayLocation { + range: Range; + label: string; } export interface InlineCompletionWarning { @@ -72,10 +79,27 @@ declare module 'vscode' { handleDidShowCompletionItem?(completionItem: InlineCompletionItem, updatedInsertText: string): void; /** - * @param completionItem The completion item that was rejected. + * Is called when an inline completion item was accepted partially. + * @param info Additional info for the partial accepted trigger. + */ + // eslint-disable-next-line local/vscode-dts-provider-naming + handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + + /** + * Is called when an inline completion item is no longer being used. + * Provides a reason of why it is not used anymore. */ // eslint-disable-next-line local/vscode-dts-provider-naming - handleDidRejectCompletionItem?(completionItem: InlineCompletionItem): void; + handleEndOfLifetime?(completionItem: InlineCompletionItem, reason: InlineCompletionEndOfLifeReason): void; + + readonly debounceDelayMs?: number; + + onDidChange?: Event; + + // #region Deprecated methods + + /** @deprecated */ + provideInlineEditsForRange?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; /** * Is called when an inline completion item was accepted partially. @@ -86,17 +110,31 @@ declare module 'vscode' { handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, acceptedLength: number): void; /** - * Is called when an inline completion item was accepted partially. - * @param info Additional info for the partial accepted trigger. - */ + * @param completionItem The completion item that was rejected. + * @deprecated Use {@link handleEndOfLifetime} instead. + */ // eslint-disable-next-line local/vscode-dts-provider-naming - handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + handleDidRejectCompletionItem?(completionItem: InlineCompletionItem): void; - provideInlineEditsForRange?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + // #endregion + } - readonly debounceDelayMs?: number; + export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2, } + export type InlineCompletionEndOfLifeReason = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; // User did an explicit action to accept + } | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; // User did an explicit action to reject + } | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: InlineCompletionItem; + userTypingDisagreed: boolean; + }; + export interface InlineCompletionContext { readonly userPrompt?: string; diff --git a/code/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts b/code/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts index e5160b12799..f365ede02e6 100644 --- a/code/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts +++ b/code/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 2 + declare module 'vscode' { export interface LanguageModelChat { @@ -21,7 +23,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - static User(content: string | Array, name?: string): LanguageModelChatMessage2; + static User(content: string | Array, name?: string): LanguageModelChatMessage2; /** * Utility to create a new assistant message. @@ -29,7 +31,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - static Assistant(content: string | Array, name?: string): LanguageModelChatMessage2; + static Assistant(content: string | Array, name?: string): LanguageModelChatMessage2; /** * The role of this message. @@ -40,7 +42,7 @@ declare module 'vscode' { * A string or heterogeneous array of things that a message can contain as content. Some parts may be message-type * specific for some models. */ - content: Array; + content: Array; /** * The optional name of a user for this message. @@ -54,23 +56,39 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); + constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); } /** - * A language model response part containing an image, returned from a {@link LanguageModelChatResponse}. - */ + * A language model response part containing arbitrary data, returned from a {@link LanguageModelChatResponse}. + */ export class LanguageModelDataPart { /** - * The image content of the part. + * Factory function to create a `LanguageModelDataPart` for an image. + * @param data Binary image data + * @param mimeType The MIME type of the image */ - value: ChatImagePart; + static image(data: Uint8Array, mimeType: ChatImageMimeType): LanguageModelDataPart; + + static json(value: object): LanguageModelDataPart; + + static text(value: string): LanguageModelDataPart; /** - * Construct an image part with the given content. - * @param value The image content of the part. + * The mime type which determines how the data property is interpreted. + */ + mimeType: string; + + /** + * The data of the part. */ - constructor(value: ChatImagePart); + data: Uint8Array; + + /** + * Construct a generic data part with the given content. + * @param value The data of the part. + */ + constructor(data: Uint8Array, mimeType: string); } /** @@ -84,15 +102,87 @@ declare module 'vscode' { BMP = 'image/bmp', } - export interface ChatImagePart { + /** + * Tagging onto this proposal, because otherwise managing two different extensions of LanguageModelChatMessage could be confusing. + * A language model response part containing arbitrary model-specific data, returned from a {@link LanguageModelChatResponse}. + * TODO@API naming, looking at LanguageModelChatRequestOptions.modelOptions, but LanguageModelModelData is not very good. + * LanguageModelOpaqueData from prompt-tsx? + */ + export class LanguageModelExtraDataPart { /** - * The image's MIME type. + * The type of data. The allowed values and data types here are model-specific. */ - mimeType: ChatImageMimeType; + kind: string; /** - * The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB. + * Extra model-specific data. */ - data: Uint8Array; + data: any; + + /** + * Construct an extra data part with the given content. + * @param value The image content of the part. + */ + constructor(kind: string, data: any); + } + + + /** + * The result of a tool call. This is the counterpart of a {@link LanguageModelToolCallPart tool call} and + * it can only be included in the content of a User message + */ + export class LanguageModelToolResultPart2 { + /** + * The ID of the tool call. + * + * *Note* that this should match the {@link LanguageModelToolCallPart.callId callId} of a tool call part. + */ + callId: string; + + /** + * The value of the tool result. + */ + content: Array; + + /** + * @param callId The ID of the tool call. + * @param content The content of the tool result. + */ + constructor(callId: string, content: Array); + } + + + /** + * A tool that can be invoked by a call to a {@link LanguageModelChat}. + */ + export interface LanguageModelTool { + /** + * Invoke the tool with the given input and return a result. + * + * The provided {@link LanguageModelToolInvocationOptions.input} has been validated against the declared schema. + */ + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken): ProviderResult; + } + + /** + * A result returned from a tool invocation. If using `@vscode/prompt-tsx`, this result may be rendered using a `ToolResult`. + */ + export class LanguageModelToolResult2 { + /** + * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in + * the future. + * @see {@link lm.invokeTool}. + */ + content: Array; + + /** + * Create a LanguageModelToolResult + * @param content A list of tool result content parts + */ + constructor(content: Array); + } + + export namespace lm { + export function invokeTool(name: string, options: LanguageModelToolInvocationOptions, token?: CancellationToken): Thenable; } } diff --git a/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 50249caa8a9..ae22bc707b2 100644 --- a/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -75,6 +75,8 @@ declare module 'vscode' { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; readonly location?: string; readonly chatRequestId?: string; + readonly chatRequestModel?: string; + readonly chatSessionId?: string; } export interface MappedEditsResponseStream { diff --git a/code/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts b/code/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts index 28a9c7b2f36..9f36dd5aca1 100644 --- a/code/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts @@ -6,40 +6,134 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/243522 + /** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and listening to its stdin and stdout streams. The process + * will be spawned as a child process of the extension host and by default + * will not run in a shell environment. + */ export class McpStdioServerDefinition { - - label: string; - + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The working directory used to start the server. + */ cwd?: Uri; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ command: string; - args: readonly string[]; + /** + * Additional command-line arguments passed to the server. + */ + args: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables. + */ env: Record; - constructor(label: string, command: string, args: string[], env: { [key: string]: string }); + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param command The command used to start the server. + * @param args Additional command-line arguments passed to the server. + * @param env Optional additional environment information for the server. + * @param version Optional version identification for the server. + */ + constructor(label: string, command: string, args?: string[], env?: Record, version?: string); } - export class McpSSEServerDefinition { - - label: string; - + /** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ + export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ uri: Uri; - headers: [string, string][]; - - constructor(label: string, uri: Uri); + /** + * Optional additional heads included with each request to the server. + */ + headers: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: Uri, headers?: Record, version?: string); } - export type McpServerDefinition = McpStdioServerDefinition | McpSSEServerDefinition; - - export interface McpConfigurationProvider { - - onDidChange?: Event; - - provideMcpServerDefinitions(token: CancellationToken): ProviderResult; - + export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + + /** + * A type that can provide server configurations. This may only be used in + * conjunction with `contributes.modelContextServerCollections` in the + * extension's package.json. + * + * To allow the editor to cache available servers, extensions should register + * this before `activate()` resolves. + */ + export interface McpServerDefinitionProvider { + /** + * Optional event fired to signal that the set of available servers has changed. + */ + onDidChangeServerDefinitions?: Event; + + /** + * Provides available MCP servers. The editor will call this method eagerly + * to ensure the availability of servers for the language model, and so + * extensions should not take actions which would require user + * interaction, such as authentication. + * + * @param token A cancellation token. + * @returns An array of MCP available MCP servers + */ + provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + + /** + * This function will be called when the editor needs to start MCP server. + * At this point, the extension may take any actions which may require user + * interaction, such as authentication. + * + * The extension may return undefined on error to indicate that the server + * should not be started. + * + * @param server The MCP server to resolve + * @param token A cancellation token. + * @returns The given, resolved server or thenable that resolves to such. + */ + resolveMcpServerDefinition?(server: T, token: CancellationToken): ProviderResult; } namespace lm { - export function registerMcpConfigurationProvider(id: string, provider: McpConfigurationProvider): Disposable; + export function registerMcpServerDefinitionProvider(id: string, provider: McpServerDefinitionProvider): Disposable; } } diff --git a/code/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/code/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 674c1ae279d..43f4c935993 100644 --- a/code/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/code/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -8,11 +8,15 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/169012 export namespace window { - export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, label: string, rootUri?: Uri): Disposable; + export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, id: string, label: string, rootUri?: Uri): Disposable; + } + + export interface SourceControl { + secondaryQuickDiffProvider?: QuickDiffProvider; } export interface QuickDiffProvider { - label?: string; - readonly visible?: boolean; + readonly id?: string; + readonly label?: string; } } diff --git a/code/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts b/code/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts deleted file mode 100644 index 6886cb1cbfe..00000000000 --- a/code/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts +++ /dev/null @@ -1,170 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/241449 - - export interface TextDocument { - - /** - * The file encoding of this document that will be used when the document is saved. - * - * Use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event to - * get notified when the document encoding changes. - * - * Note that the possible encoding values are currently defined as any of the following: - * 'utf8', 'utf8bom', 'utf16le', 'utf16be', 'windows1252', 'iso88591', 'iso88593', - * 'iso885915', 'macroman', 'cp437', 'windows1256', 'iso88596', 'windows1257', - * 'iso88594', 'iso885914', 'windows1250', 'iso88592', 'cp852', 'windows1251', - * 'cp866', 'cp1125', 'iso88595', 'koi8r', 'koi8u', 'iso885913', 'windows1253', - * 'iso88597', 'windows1255', 'iso88598', 'iso885910', 'iso885916', 'windows1254', - * 'iso88599', 'windows1258', 'gbk', 'gb18030', 'cp950', 'big5hkscs', 'shiftjis', - * 'eucjp', 'euckr', 'windows874', 'iso885911', 'koi8ru', 'koi8t', 'gb2312', - * 'cp865', 'cp850'. - */ - readonly encoding: string; - } - - export namespace workspace { - - /** - * Opens a document. Will return early if this document is already open. Otherwise - * the document is loaded and the {@link workspace.onDidOpenTextDocument didOpen}-event fires. - * - * The document is denoted by an {@link Uri}. Depending on the {@link Uri.scheme scheme} the - * following rules apply: - * * `file`-scheme: Open a file on disk (`openTextDocument(Uri.file(path))`). Will be rejected if the file - * does not exist or cannot be loaded. - * * `untitled`-scheme: Open a blank untitled file with associated path (`openTextDocument(Uri.file(path).with({ scheme: 'untitled' }))`). - * The language will be derived from the file name. - * * For all other schemes contributed {@link TextDocumentContentProvider text document content providers} and - * {@link FileSystemProvider file system providers} are consulted. - * - * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an - * {@linkcode workspace.onDidCloseTextDocument onDidClose}-event can occur at any time after opening it. - * - * @throws This method will throw an error when an existing text document with the provided uri is dirty. - * - * @param uri Identifies the resource to open. - * @param options Options to control how the document will be opened. - * @returns A promise that resolves to a {@link TextDocument document}. - */ - export function openTextDocument(uri: Uri, options?: { - /** - * The {@link TextDocument.encoding encoding} of the document to use - * for decoding the underlying buffer to text. If omitted, the encoding - * will be guessed based on the file content and/or the editor settings - * unless the document is already opened. - * - * See {@link TextDocument.encoding} for more information about valid - * values for encoding. - * - * *Note* that opening a text document that was already opened with a - * different encoding has the potential of changing the text contents of - * the text document. Specifically, when the encoding results in a - * different set of characters than the previous encoding. - * - * *Note* that if you open a document with an encoding that does not - * support decoding the underlying bytes, content may be replaced with - * substitution characters as appropriate. - */ - encoding?: string; - }): Thenable; - - /** - * A short-hand for `openTextDocument(Uri.file(path))`. - * - * @see {@link workspace.openTextDocument} - * @param path A path of a file on disk. - * @param options Options to control how the document will be opened. - * @returns A promise that resolves to a {@link TextDocument document}. - */ - export function openTextDocument(path: string, options?: { - /** - * The {@link TextDocument.encoding encoding} of the document to use - * for decoding the underlying buffer to text. If omitted, the encoding - * will be guessed based on the file content and/or the editor settings - * unless the document is already opened. - * - * See {@link TextDocument.encoding} for more information about valid - * values for encoding. - * - * *Note* that opening a text document that was already opened with a - * different encoding has the potential of changing the text contents of - * the text document. Specifically, when the encoding results in a - * different set of characters than the previous encoding. - * - * *Note* that if you open a document with an encoding that does not - * support decoding the underlying bytes, content may be replaced with - * substitution characters as appropriate. - */ - encoding?: string; - }): Thenable; - - /** - * Opens an untitled text document. The editor will prompt the user for a file - * path when the document is to be saved. The `options` parameter allows to - * specify the *language*, *encoding* and/or the *content* of the document. - * - * @param options Options to control how the document will be created. - * @returns A promise that resolves to a {@link TextDocument document}. - */ - export function openTextDocument(options?: { - /** - * The {@link TextDocument.languageId language} of the document. - */ - language?: string; - /** - * The initial contents of the document. - */ - content?: string; - /** - * The {@link TextDocument.encoding encoding} of the document. - */ - encoding?: string; - }): Thenable; - - /** - * Decodes the content from a `Uint8Array` to a `string`. You MUST - * provide the entire content at once to ensure that the encoding - * can properly apply. Do not use this method to decode content - * in chunks, as that may lead to incorrect results. - * - * If no encoding is provided, will try to pick an encoding based - * on user settings and the content of the buffer (for example - * byte order marks). - * - * *Note* that if you decode content that is unsupported by the - * encoding, the result may contain substitution characters as - * appropriate. - * - * @throws This method will throw an error when the content is binary. - * - * @param content The content to decode as a `Uint8Array`. - * @param uri The URI that represents the file. This information - * is used to figure out the encoding related configuration for the file. - * @param options Allows to explicitly pick the encoding to use. See {@link TextDocument.encoding} - * for more information about valid values for encoding. - * @returns A thenable that resolves to the decoded `string`. - */ - export function decode(content: Uint8Array, uri: Uri | undefined, options?: { encoding: string }): Thenable; - - /** - * Encodes the content of a `string` to a `Uint8Array`. - * - * If no encoding is provided, will try to pick an encoding based - * on user settings. - * - * @param content The content to decode as a `string`. - * @param uri The URI that represents the file. This information - * is used to figure out the encoding related configuration for the file. - * @param options Allows to explicitly pick the encoding to use. See {@link TextDocument.encoding} - * for more information about valid values for encoding. - * @returns A thenable that resolves to the encoded `Uint8Array`. - */ - export function encode(content: string, uri: Uri | undefined, options?: { encoding: string }): Thenable; - } -} diff --git a/code/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts b/code/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts index 146b76b5fa5..2bcb81299b8 100644 --- a/code/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts +++ b/code/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts @@ -237,12 +237,34 @@ declare module 'vscode' { lineNumber: number; } + /** + * Keyword suggestion for AI search. + */ + export class AISearchKeyword { + /** + * @param keyword The keyword associated with the search. + */ + constructor(keyword: string); + + /** + * The keyword associated with the search. + */ + keyword: string; + } + /** * A result payload for a text search, pertaining to {@link TextSearchMatch2 matches} * and its associated {@link TextSearchContext2 context} within a single file. */ export type TextSearchResult2 = TextSearchMatch2 | TextSearchContext2; + /** + * A result payload for an AI search. + * This can be a {@link TextSearchMatch2 match} or a {@link AISearchKeyword keyword}. + * The result can be a match or a keyword. + */ + export type AISearchResult = TextSearchResult2 | AISearchKeyword; + /** * A TextSearchProvider provides search results for text results inside files in the workspace. */ @@ -255,7 +277,7 @@ declare module 'vscode' { * These results can be direct matches, or context that surrounds matches. * @param token A cancellation token. */ - provideTextSearchResults(query: TextSearchQuery2, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult; + provideTextSearchResults(query: TextSearchQuery2, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult; } export namespace workspace { diff --git a/code/src/vscode-dts/vscode.proposed.toolProgress.d.ts b/code/src/vscode-dts/vscode.proposed.toolProgress.d.ts new file mode 100644 index 00000000000..0d20f626cc1 --- /dev/null +++ b/code/src/vscode-dts/vscode.proposed.toolProgress.d.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * A progress update during an {@link LanguageModelTool.invoke} call. + */ + export interface ToolProgressStep { + /** + * A progress message that represents a chunk of work + */ + message?: string | MarkdownString; + /** + * An increment for discrete progress. Increments will be summed up until 100 (100%) is reached + */ + increment?: number; + } + + export interface LanguageModelTool { + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken, progress: Progress): ProviderResult; + } +} diff --git a/code/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts b/code/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts index eab3ffa5d2e..aa5c4b7739c 100644 --- a/code/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts +++ b/code/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts @@ -31,7 +31,7 @@ declare module 'vscode' { export interface TunnelProvider { /** - * Provides port forwarding capabilities. If there is a resolver that already provids tunnels, then the resolver's provider will + * Provides port forwarding capabilities. If there is a resolver that already provides tunnels, then the resolver's provider will * be used. If multiple providers are registered, then only the first will be used. */ provideTunnel(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions, token: CancellationToken): ProviderResult; diff --git a/code/test/automation/src/code.ts b/code/test/automation/src/code.ts index 75b8a4980ec..ae612626c9e 100644 --- a/code/test/automation/src/code.ts +++ b/code/test/automation/src/code.ts @@ -5,7 +5,6 @@ import * as cp from 'child_process'; import * as os from 'os'; -import * as treekill from 'tree-kill'; import { IElement, ILocaleInfo, ILocalizedStrings, ILogFile } from './driver'; import { Logger, measureAndLog } from './logger'; import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; @@ -22,14 +21,14 @@ export interface LaunchOptions { readonly logger: Logger; logsPath: string; crashesPath: string; - readonly verbose?: boolean; + verbose?: boolean; readonly extraArgs?: string[]; readonly remote?: boolean; readonly web?: boolean; 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; } @@ -39,18 +38,28 @@ interface ICodeInstance { const instances = new Set(); -function registerInstance(process: cp.ChildProcess, logger: Logger, type: string) { +function registerInstance(process: cp.ChildProcess, logger: Logger, type: 'electron' | 'server'): { safeToKill: Promise } { const instance = { kill: () => teardown(process, logger) }; instances.add(instance); - process.stdout?.on('data', data => logger.log(`[${type}] stdout: ${data}`)); - process.stderr?.on('data', error => logger.log(`[${type}] stderr: ${error}`)); + const safeToKill = new Promise(resolve => { + process.stdout?.on('data', data => { + const output = data.toString(); + if (output.indexOf('calling app.quit()') >= 0 && type === 'electron') { + setTimeout(() => resolve(), 500 /* give Electron some time to actually terminate fully */); + } + logger.log(`[${type}] stdout: ${output}`); + }); + process.stderr?.on('data', error => logger.log(`[${type}] stderr: ${error}`)); + }); process.once('exit', (code, signal) => { logger.log(`[${type}] Process terminated (pid: ${process.pid}, code: ${code}, signal: ${signal})`); instances.delete(instance); }); + + return { safeToKill }; } async function teardownAll(signal?: number) { @@ -80,15 +89,15 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess, options.quality); + return new Code(driver, options.logger, serverProcess, undefined, options.quality); } // Electron smoke tests (playwright) else { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); - registerInstance(electronProcess, options.logger, 'electron'); + const { safeToKill } = registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess, options.quality); + return new Code(driver, options.logger, electronProcess, safeToKill, options.quality); } } @@ -100,6 +109,7 @@ export class Code { driver: PlaywrightDriver, readonly logger: Logger, private readonly mainProcess: cp.ChildProcess, + private readonly safeToKill: Promise | undefined, readonly quality: Quality ) { this.driver = new Proxy(driver, { @@ -144,7 +154,13 @@ export class Code { let done = false; // Start the exit flow via driver - this.driver.exitApplication(); + this.driver.close(); + + let safeToKill = false; + this.safeToKill?.then(() => { + this.logger.log('Smoke test exit(): safeToKill() called'); + safeToKill = true; + }); // Await the exit of the application (async () => { @@ -152,38 +168,27 @@ export class Code { while (!done) { retries++; + if (safeToKill) { + this.logger.log('Smoke test exit(): call did not terminate the process yet, but safeToKill is true, so we can kill it'); + this.kill(pid); + } + switch (retries) { - // after 5 / 10 seconds: try to exit gracefully again - case 10: + // after 10 seconds: forcefully kill case 20: { - this.logger.log('Smoke test exit call did not terminate process after 5-10s, gracefully trying to exit the application again...'); - this.driver.exitApplication(); + this.logger.log('Smoke test exit(): call did not terminate process after 10s, forcefully exiting the application...'); + this.kill(pid); break; } - // after 20 seconds: forcefully kill + // after 20 seconds: give up 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 - } - }); - - break; - } - - // after 30 seconds: give up - case 60: { + this.logger.log('Smoke test exit(): call did not terminate process after 20s, giving up'); + this.kill(pid); done = true; - this.logger.log('Smoke test exit call did not terminate process after 30s, giving up'); resolve(); + break; } } @@ -191,6 +196,8 @@ export class Code { process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. await this.wait(500); } catch (error) { + this.logger.log('Smoke test exit(): call terminated process successfully'); + done = true; resolve(); } @@ -199,6 +206,22 @@ export class Code { }), 'Code#exit()', this.logger); } + private kill(pid: number): void { + try { + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. + } catch (e) { + this.logger.log('Smoke test kill(): returning early because process does not exist anymore'); + return; + } + + try { + this.logger.log(`Smoke test kill(): Trying to SIGTERM process: ${pid}`); + process.kill(pid); + } catch (e) { + this.logger.log('Smoke test kill(): SIGTERM failed', e); + } + } + async getElement(selector: string): Promise { return (await this.driver.getElements(selector))?.[0]; } diff --git a/code/test/automation/src/electron.ts b/code/test/automation/src/electron.ts index 8a9a73974f6..7d162daf1d2 100644 --- a/code/test/automation/src/electron.ts +++ b/code/test/automation/src/electron.ts @@ -41,25 +41,6 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom args.push('--verbose'); } - if (process.platform === 'linux') { - // --disable-dev-shm-usage: when run on docker containers where size of /dev/shm - // partition < 64MB which causes OOM failure for chromium compositor that uses - // this partition for shared memory. - // Refs https://github.com/microsoft/vscode/issues/152143 - args.push('--disable-dev-shm-usage'); - // Refs https://github.com/microsoft/vscode/issues/192206 - args.push('--disable-gpu'); - } - - if (process.platform === 'darwin') { - // On macOS force software based rendering since we are seeing GPU process - // hangs when initializing GL context. This is very likely possible - // that there are new displays available in the CI hardware and - // the relevant drivers couldn't be loaded via the GPU sandbox. - // TODO(deepak1556): remove this switch with Electron update. - args.push('--use-gl=swiftshader'); - } - if (remote) { // Replace workspace path with URI args[0] = `--${workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(workspacePath).path}`; diff --git a/code/test/automation/src/extensions.ts b/code/test/automation/src/extensions.ts index 3713faa6700..06bd324465f 100644 --- a/code/test/automation/src/extensions.ts +++ b/code/test/automation/src/extensions.ts @@ -59,7 +59,7 @@ export class Extensions extends Viewlet { await this.code.waitAndClick(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"] .extension-list-item .monaco-action-bar .action-item:not(.disabled) .extension-action.install`); try { - await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`); + await this.waitForExtensionToBeInstalled(); break; } catch (err) { if (attempt++ === 3) { @@ -72,6 +72,25 @@ export class Extensions extends Viewlet { await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) a[aria-label="Disable this extension"]`); } } + + private async waitForExtensionToBeInstalled(): Promise { + let attempt = 1; + while (true) { + try { + await this.code.waitForElement(`.extension-editor .monaco-action-bar .action-item:not(.disabled) .extension-action.uninstall`, undefined); + break; + } catch (err) { + if (await this.code.getElement(`.extension-editor .monaco-action-bar .action-item .extension-action.install.installing`)) { + if (attempt++ === 3) { + throw err; + } + this.code.logger.log('Extension is still being installed. Waiting...'); + } else { + throw err; + } + } + } + } } export async function copyExtension(repoPath: string, extensionsPath: string, extId: string): Promise { diff --git a/code/test/automation/src/playwrightBrowser.ts b/code/test/automation/src/playwrightBrowser.ts index 044da21bc9f..f4f63875b2a 100644 --- a/code/test/automation/src/playwrightBrowser.ts +++ b/code/test/automation/src/playwrightBrowser.ts @@ -90,10 +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, - args: ['--headless=new'], - timeout: 0 + timeout: 0, + channel: browserChannel, }), 'playwright#launch', logger); browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`)); diff --git a/code/test/automation/src/playwrightDriver.ts b/code/test/automation/src/playwrightDriver.ts index 018fe7c351a..a6fd72754f9 100644 --- a/code/test/automation/src/playwrightDriver.ts +++ b/code/test/automation/src/playwrightDriver.ts @@ -174,7 +174,7 @@ export class PlaywrightDriver { await this.page.reload(); } - async exitApplication() { + async close() { // Stop tracing try { @@ -194,22 +194,11 @@ export class PlaywrightDriver { } } - // Web: exit via `close` method - if (this.options.web) { - try { - await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger); - } catch (error) { - this.options.logger.log(`Error closing appliction (${error})`); - } - } - - // Desktop: exit via `driver.exitApplication` - else { - try { - await measureAndLog(() => this.evaluateWithDriver(([driver]) => driver.exitApplication()), 'driver.exitApplication()', this.options.logger); - } catch (error) { - this.options.logger.log(`Error exiting appliction (${error})`); - } + // exit via `close` method + try { + await measureAndLog(() => this.application.close(), 'playwright.close()', this.options.logger); + } catch (error) { + this.options.logger.log(`Error closing application (${error})`); } // Server: via `teardown` @@ -329,6 +318,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/code/test/integration/browser/src/index.ts b/code/test/integration/browser/src/index.ts index 308878d0d8e..0c3cd8efd32 100644 --- a/code/test/integration/browser/src/index.ts +++ b/code/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), args: ['--headless=new'], }); +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/code/test/smoke/src/areas/extensions/extensions.test.ts b/code/test/smoke/src/areas/extensions/extensions.test.ts index c20700cbc91..875c61d6a4b 100644 --- a/code/test/smoke/src/areas/extensions/extensions.test.ts +++ b/code/test/smoke/src/areas/extensions/extensions.test.ts @@ -11,6 +11,7 @@ export function setup(logger: Logger) { // Shared before/after handling installAllHandlers(logger, opts => { + opts.verbose = true; // enable verbose logging for tracing opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension return opts; }); diff --git a/code/test/smoke/src/areas/workbench/localization.test.ts b/code/test/smoke/src/areas/workbench/localization.test.ts index c3ce9b04fc2..74d3ea3acf9 100644 --- a/code/test/smoke/src/areas/workbench/localization.test.ts +++ b/code/test/smoke/src/areas/workbench/localization.test.ts @@ -12,6 +12,7 @@ export function setup(logger: Logger) { // Shared before/after handling installAllHandlers(logger, opts => { + opts.verbose = true; // enable verbose logging for tracing opts.snapshots = true; // enable network tab in devtools for tracing since we install an extension return opts; }); diff --git a/code/test/unit/browser/index.js b/code/test/unit/browser/index.js index f19c21e46c4..e5cf68ccab5 100644 --- a/code/test/unit/browser/index.js +++ b/code/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 @@ -151,7 +151,7 @@ const testModules = (async function () { modules.push(file.replace(/\.js$/, '')); } else if (!isDefaultModules) { - console.warn(`DROPPONG ${file} because it cannot be run inside a browser`); + console.warn(`DROPPING ${file} because it cannot be run inside a browser`); } } return modules; @@ -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) { diff --git a/code/test/unit/electron/renderer.js b/code/test/unit/electron/renderer.js index aa4f05984a2..661be873561 100644 --- a/code/test/unit/electron/renderer.js +++ b/code/test/unit/electron/renderer.js @@ -372,7 +372,7 @@ function safeStringify(obj) { function isObject(obj) { // The method can't do a type cast since there are type (like strings) which - // are subclasses of any put not positvely matched by the function. Hence type + // are subclasses of any put not positively matched by the function. Hence type // narrowing results in wrong results. return typeof obj === 'object' && obj !== null diff --git a/rebase.sh b/rebase.sh index e0fea0acb67..832bfd56a0e 100755 --- a/rebase.sh +++ b/rebase.sh @@ -476,18 +476,12 @@ resolve_conflicts() { apply_changes "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts" ]]; then apply_changes "$conflictingFile" - elif [[ "$conflictingFile" == "code/test/automation/src/playwrightBrowser.ts" ]]; then - apply_changes "$conflictingFile" - elif [[ "$conflictingFile" == "code/test/integration/browser/src/index.ts" ]]; then - apply_changes "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts" ]]; then apply_changes "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts" ]]; then apply_changes "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/platform/extensionManagement/node/extensionManagementService.ts" ]]; then apply_changes "$conflictingFile" - elif [[ "$conflictingFile" == "code/src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts" ]]; then - apply_changes "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts" ]]; then apply_multi_line_replace "$conflictingFile" else @@ -516,7 +510,7 @@ do_rebase() { echo "Using git $(which git) $(git --version)" # grab current upstream version - UPSTREAM_VERSION=$(git rev-parse upstream-code/main) + UPSTREAM_VERSION=$(git rev-parse upstream-code/release/1.100) #UPSTREAM_VERSION=1.62.2 # Grab current version