From 9207b53cf370be498f1dd033d8c442034e707f41 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:58:46 -0800 Subject: [PATCH 01/27] @xterm/xterm@5.6.0-beta.74 Fixes #117741 --- package-lock.json | 86 ++++++++++++++++++------------------ package.json | 18 ++++---- remote/package-lock.json | 86 ++++++++++++++++++------------------ remote/package.json | 18 ++++---- remote/web/package-lock.json | 78 ++++++++++++++++---------------- remote/web/package.json | 16 +++---- 6 files changed, 151 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f2b14c3e39..9d8cbea7bb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,15 +27,15 @@ "@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.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -3457,30 +3457,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3490,55 +3490,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.70.tgz", - "integrity": "sha512-npSJzmtJq8LLAzV3nwD9KkBQLNWSusi+cOBPyc3zlYYE63Vkqbtbp/iQS27zH2GxJ95rzCzDwnX6VOn0OoUPYQ==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", + "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { diff --git a/package.json b/package.json index 3316ae225ea..4cbd98ecdd5 100644 --- a/package.json +++ b/package.json @@ -85,15 +85,15 @@ "@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.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index 3cf22939861..53a1dddc292 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -20,15 +20,15 @@ "@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.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -520,30 +520,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -553,55 +553,55 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.70.tgz", - "integrity": "sha512-npSJzmtJq8LLAzV3nwD9KkBQLNWSusi+cOBPyc3zlYYE63Vkqbtbp/iQS27zH2GxJ95rzCzDwnX6VOn0OoUPYQ==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.74.tgz", + "integrity": "sha512-6PSNk1/CaLGNZLhXULswjfbf/rJrG1EomT9hR2nNbX6Osm9LVbhgIzg/mYHPu9wX5Pz7Gpd1VWp4/1NcKpN53w==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/agent-base": { diff --git a/remote/package.json b/remote/package.json index 5a61a15c051..09dc79f4b0a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -15,15 +15,15 @@ "@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.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/headless": "^5.6.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/headless": "^5.6.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 3e45e62a14d..6712d40c628 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,14 +13,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -88,30 +88,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.53", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.53.tgz", - "integrity": "sha512-kCcBuGvF8mwzExU+Tm9eylPvp1kXTkvm+kO0V4qP7HI3ZCw5vfKmnlRn41FvNIylsK2hnmrFtxauPHEGBy/dfA==", + "version": "0.2.0-beta.57", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.57.tgz", + "integrity": "sha512-/GSI8Fkmb8s/V1t2EGc2U2PUfSqge6f9gAeob65EwarsfBf66cmCxMG0ZSPE8+nti1pGIsrJA8XfeEaJt4clcA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.70.tgz", - "integrity": "sha512-QLhy77i0sjnffkLuxj1yB/mBUJI64bbL86eMW+1g5XEsZnSevY8YwU/cEJg02PAGK0ggwQNNfiRwoO6VBCdYFg==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.74.tgz", + "integrity": "sha512-mJPWNPov2mqrUkYZCs6UCn5p6DBLeN6xjpLu5mLh8cmXr544VWfqEVNAYPQ9+8uNgXdzSsKInBv9ZGtbXV0SfA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.70.tgz", - "integrity": "sha512-BPDHHOybUWO6mjHf/RMDBjSKDl9QdyyGTyHvmlyhuI/2sma3lu98bA4U03F8nBnvj/6otzEFARuOeoN7rkfRng==", + "version": "0.10.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.74.tgz", + "integrity": "sha512-/X3OemVqPSgdely8OgdQb0cJqv9HqiMaBLeLe2QHfTWdXDBOLG/O4g8n/lChqR9rulEMwPCt2LqvtyIMA3iZsw==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -121,49 +121,49 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.70.tgz", - "integrity": "sha512-uaNBf77cr5Jikj69TDkTfe3V3wHA+4tDTFcv8DwJ/KGORPLUfBk8cn9HpbIJ+0jc1kOZr5xDrEjaYNwEVDkGeQ==", + "version": "0.16.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.74.tgz", + "integrity": "sha512-KMOeOu3EvtDWcH/HCs6fe4KyaMMdjoUjP1C7R3AtZwJVdm5GaFxASxdol+9evfGhUUei4qt+zVsaFraNKyFvsA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.70.tgz", - "integrity": "sha512-4ijqHU7xDRcZ4Gm6yN/tKsZzovUwzttE9/pPfhxpWIgSdm9c8i5R4rGyOAlAZCnuU3DT5vPplQO7W/0zwAmJsQ==", + "version": "0.14.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.74.tgz", + "integrity": "sha512-zBhQAoXagBMhKFGYQ6n52sKapY61Jt3hKT8awhG/49f04JkaX1Jhc6xohD+aZs6J2ABHI7EWW/y0xTN9BDLLBw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.70.tgz", - "integrity": "sha512-9UE4v2SpWtqd85Hqvk8LW4QA9Phe93RMVltrNtt9jCUmkAok/QLFOEbLqoq2JUOvlmfsYfXmIl11Wg4CYIsvwA==", + "version": "0.9.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.74.tgz", + "integrity": "sha512-1AyuDST77Xg33ed+3neNrQfsfZVwa+C16uWP9eTdJ1rO48ylqPcFTDnahCfIZGPHGjPqLl90ZKZ8tt1zWSefmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.70.tgz", - "integrity": "sha512-3BkmF24i66SHCfAkcHy1VC6H715qyelfOIjd6n7sTqHon56J1bUO782Q1al/MK0vSvplElBRKVdR31SDvITzpg==", + "version": "0.19.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.74.tgz", + "integrity": "sha512-1wKiuv6WHMqOcSlLeIRU7UF8zkU4KU35rnPvLw2G45aAJSr9B3f7EVIaJ4IjXGeY/C+WCM3wWuybPN5VdB8qAQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.70" + "@xterm/xterm": "^5.6.0-beta.74" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.70", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.70.tgz", - "integrity": "sha512-qviQMVWhRtgPn4z8PHNH6D/ffSKkNBHmUX1HyJxl325QM2xF8M8met83uFv7JZm7a5OQYScnLGsFAoTreSgdew==", + "version": "5.6.0-beta.74", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.74.tgz", + "integrity": "sha512-gVu7+4Cfd7O/6cQ/UK0sZM+TJBaI/VgQ/JFAhuAnNFj29wts2MzxSH3fIp3KUG1kqlJBWEUShCTwB8nKwtbCjQ==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 8c1decd65bb..31f552e7f5c 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,14 +8,14 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.53", - "@xterm/addon-image": "^0.9.0-beta.70", - "@xterm/addon-ligatures": "^0.10.0-beta.70", - "@xterm/addon-search": "^0.16.0-beta.70", - "@xterm/addon-serialize": "^0.14.0-beta.70", - "@xterm/addon-unicode11": "^0.9.0-beta.70", - "@xterm/addon-webgl": "^0.19.0-beta.70", - "@xterm/xterm": "^5.6.0-beta.70", + "@xterm/addon-clipboard": "^0.2.0-beta.57", + "@xterm/addon-image": "^0.9.0-beta.74", + "@xterm/addon-ligatures": "^0.10.0-beta.74", + "@xterm/addon-search": "^0.16.0-beta.74", + "@xterm/addon-serialize": "^0.14.0-beta.74", + "@xterm/addon-unicode11": "^0.9.0-beta.74", + "@xterm/addon-webgl": "^0.19.0-beta.74", + "@xterm/xterm": "^5.6.0-beta.74", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", From 5cc442a4186e7cf97d3809ddc23031b8839f2fce Mon Sep 17 00:00:00 2001 From: Dmitry Sonder Date: Wed, 25 Dec 2024 14:13:11 +0700 Subject: [PATCH 02/27] refactor: use EventType constants for events --- src/vs/editor/browser/editorDom.ts | 6 +++--- .../electron-sandbox/parts/titlebar/titlebarPart.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 38100ab8407..e42959d81b6 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -150,13 +150,13 @@ export class EditorMouseEventFactory { } public onContextMenu(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'contextmenu', (e: MouseEvent) => { + return dom.addDisposableListener(target, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => { callback(this._create(e)); }); } public onMouseUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'mouseup', (e: MouseEvent) => { + return dom.addDisposableListener(target, dom.EventType.MOUSE_UP, (e: MouseEvent) => { callback(this._create(e)); }); } @@ -180,7 +180,7 @@ export class EditorMouseEventFactory { } public onMouseMove(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable { - return dom.addDisposableListener(target, 'mousemove', (e) => callback(this._create(e))); + return dom.addDisposableListener(target, dom.EventType.MOUSE_MOVE, (e) => callback(this._create(e))); } } diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index a4f1a964afe..04095aab5d7 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -200,7 +200,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { } const zoomFactor = getZoomFactor(getWindow(this.element)); - this.onContextMenu(new MouseEvent('mouseup', { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext); + this.onContextMenu(new MouseEvent(EventType.MOUSE_UP, { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext); })); } From d8d429c47285632fa46c4acd5728a3ef23363675 Mon Sep 17 00:00:00 2001 From: Andrew Suzuki Date: Fri, 3 Jan 2025 12:49:45 -0500 Subject: [PATCH 03/27] Fix new Color in string typo --- src/vs/editor/common/core/editorColorRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 1bcf7252443..15679ff7ba9 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -80,7 +80,7 @@ export const editorBracketHighlightingForeground4 = registerColor('editorBracket export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', '#00000000', nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5). Requires enabling bracket pair colorization.')); export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', '#00000000', nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6). Requires enabling bracket pair colorization.')); -export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: 'new Color(new RGBA(255, 50, 50, 1))', hcLight: '#B5200D' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); +export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.')); export const editorBracketPairGuideBackground1 = registerColor('editorBracketPairGuide.background1', '#00000000', nls.localize('editorBracketPairGuide.background1', 'Background color of inactive bracket pair guides (1). Requires enabling bracket pair guides.')); export const editorBracketPairGuideBackground2 = registerColor('editorBracketPairGuide.background2', '#00000000', nls.localize('editorBracketPairGuide.background2', 'Background color of inactive bracket pair guides (2). Requires enabling bracket pair guides.')); From 08c8f1a330274cb42964a8fb5813a2e593965b82 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 6 Jan 2025 14:58:08 +1100 Subject: [PATCH 04/27] Sort cell metadata for notebook cell diffview (#237302) --- .../notebook/browser/diff/diffComponents.ts | 4 +-- .../notebook/browser/notebook.contribution.ts | 6 ++--- .../common/model/notebookCellTextModel.ts | 25 +++++++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 3ef8a33b088..a0a5557ddab 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -847,7 +847,7 @@ abstract class AbstractElementRenderer extends Disposable { return; } - const modifiedMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.modified?.metadata || {}, this.cell.modified?.language); + const modifiedMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.modified?.metadata || {}, this.cell.modified?.language, true); modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource); })); @@ -869,7 +869,7 @@ abstract class AbstractElementRenderer extends Disposable { const originalMetadataSource = getFormattedMetadataJSON(this.notebookEditor.textModel?.transientOptions.transientCellMetadata, this.cell.type === 'insert' ? this.cell.modified!.metadata || {} - : this.cell.original!.metadata || {}); + : this.cell.original!.metadata || {}, undefined, true); const uri = this.cell.type === 'insert' ? this.cell.modified!.uri : this.cell.original!.uri; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a09069d24cf..17568757a13 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -452,7 +452,7 @@ class CellInfoContentProvider { for (const cell of ref.object.notebook.cells) { if (cell.handle === data.handle) { const cellIndex = ref.object.notebook.cells.indexOf(cell); - const metadataSource = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language); + const metadataSource = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language, true); result = this._modelService.createModel( metadataSource, mode, @@ -460,9 +460,9 @@ class CellInfoContentProvider { ); this._disposables.push(disposables.add(ref.object.notebook.onDidChangeContent(e => { if (result && e.rawEvents.some(event => (event.kind === NotebookCellsChangeType.ChangeCellMetadata || event.kind === NotebookCellsChangeType.ChangeCellLanguage) && event.index === cellIndex)) { - const value = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language); + const value = getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language, true); if (result.getValue() !== value) { - result.setValue(getFormattedMetadataJSON(ref.object.notebook.transientOptions.transientCellMetadata, cell.metadata, cell.language)); + result.setValue(value); } } }))); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 018aa490c9a..ac481402ced 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -516,7 +516,7 @@ function computeRunStartTimeAdjustment(oldMetadata: NotebookCellInternalMetadata } -export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMetadata | undefined, metadata: NotebookCellMetadata, language?: string) { +export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMetadata | undefined, metadata: NotebookCellMetadata, language?: string, sortKeys?: boolean): string { let filteredMetadata: { [key: string]: any } = {}; if (transientCellMetadata) { @@ -541,7 +541,28 @@ export function getFormattedMetadataJSON(transientCellMetadata: TransientCellMet if (language) { obj.language = language; } - const metadataSource = toFormattedString(obj, {}); + const metadataSource = toFormattedString(sortKeys ? sortObjectPropertiesRecursively(obj) : obj, {}); return metadataSource; } + + +/** + * Sort the JSON to ensure when diffing, the JSON keys are sorted & matched correctly in diff view. + */ +export function sortObjectPropertiesRecursively(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(sortObjectPropertiesRecursively); + } + if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { + return ( + Object.keys(obj) + .sort() + .reduce>((sortedObj, prop) => { + sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); + return sortedObj; + }, {}) as any + ); + } + return obj; +} From 8cc255e03ac7c51804cea1194b1ea864bbe2ccb6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 6 Jan 2025 14:58:34 +1100 Subject: [PATCH 05/27] Ensure execution_count is cleared when clearing outputs (#237301) --- extensions/ipynb/src/deserializers.ts | 8 +- .../ipynb/src/notebookModelStoreSync.ts | 7 + .../ipynb/src/test/clearOutputs.test.ts | 736 ++++++++++++++++++ extensions/ipynb/src/test/serializers.test.ts | 13 +- .../notebook/browser/notebookEditorWidget.ts | 2 +- 5 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 extensions/ipynb/src/test/clearOutputs.test.ts diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts index de467f66077..ad99a4fcea8 100644 --- a/extensions/ipynb/src/deserializers.ts +++ b/extensions/ipynb/src/deserializers.ts @@ -149,8 +149,12 @@ function getNotebookCellMetadata(cell: nbformat.IBaseCell): { // We put this only for VSC to display in diff view. // Else we don't use this. const cellMetadata: CellMetadata = {}; - if (cell.cell_type === 'code' && typeof cell['execution_count'] === 'number') { - cellMetadata.execution_count = cell['execution_count']; + if (cell.cell_type === 'code') { + if (typeof cell['execution_count'] === 'number') { + cellMetadata.execution_count = cell['execution_count']; + } else { + cellMetadata.execution_count = null; + } } if (cell['metadata']) { diff --git a/extensions/ipynb/src/notebookModelStoreSync.ts b/extensions/ipynb/src/notebookModelStoreSync.ts index dc1bae1de2f..836e1c8afc5 100644 --- a/extensions/ipynb/src/notebookModelStoreSync.ts +++ b/extensions/ipynb/src/notebookModelStoreSync.ts @@ -165,6 +165,13 @@ function onDidChangeNotebookCells(e: NotebookDocumentChangeEventEx) { metadata.execution_count = null; metadataUpdated = true; pendingCellUpdates.delete(e.cell); + } else if (!e.executionSummary?.executionOrder && !e.executionSummary?.success && !e.executionSummary?.timing + && !e.metadata && !e.outputs && currentMetadata.execution_count && !pendingCellUpdates.has(e.cell)) { + // This is a result of the cell without outupts but has execution count being cleared + // Create two cells, one that produces output and one that doesn't. Run both and then clear the output or all cells. + // This condition will be satisfied for first cell without outputs. + metadata.execution_count = null; + metadataUpdated = true; } if (e.document?.languageId && e.document?.languageId !== preferredCellLanguage && e.document?.languageId !== languageIdInMetadata) { diff --git a/extensions/ipynb/src/test/clearOutputs.test.ts b/extensions/ipynb/src/test/clearOutputs.test.ts new file mode 100644 index 00000000000..0437e9838bb --- /dev/null +++ b/extensions/ipynb/src/test/clearOutputs.test.ts @@ -0,0 +1,736 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from 'sinon'; +import type * as nbformat from '@jupyterlab/nbformat'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { jupyterNotebookModelToNotebookData } from '../deserializers'; +import { activate } from '../notebookModelStoreSync'; + + +suite(`ipynb Clear Outputs`, () => { + const disposables: vscode.Disposable[] = []; + const context = { subscriptions: disposables } as vscode.ExtensionContext; + setup(() => { + disposables.length = 0; + activate(context); + }); + teardown(async () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + sinon.restore(); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('Clear outputs after opening Notebook', async () => { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [{ output_type: 'stream', name: 'stdout', text: ['Hello'] }], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'code', + outputs: [], + source: 'print(2)', + metadata: {} + }, + { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + const notebookDocument = await vscode.workspace.openNotebookDocument('jupyter-notebook', notebook); + await vscode.window.showNotebookDocument(notebookDocument); + + assert.strictEqual(notebookDocument.cellCount, 3); + assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, 10); + assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined); + + // Clear all outputs + await vscode.commands.executeCommand('notebook.clearAllCellsOutputs'); + + // Wait for all changes to be applied, could take a few ms. + const verifyMetadataChanges = () => { + assert.strictEqual(notebookDocument.cellAt(0).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(1).metadata.execution_count, null); + assert.strictEqual(notebookDocument.cellAt(2).metadata.execution_count, undefined); + }; + + vscode.workspace.onDidChangeNotebookDocument(() => verifyMetadataChanges(), undefined, disposables); + + await new Promise((resolve, reject) => { + const interval = setInterval(() => { + try { + verifyMetadataChanges(); + clearInterval(interval); + resolve(); + } catch { + // Ignore + } + }, 50); + disposables.push({ dispose: () => clearInterval(interval) }); + const timeout = setTimeout(() => { + try { + verifyMetadataChanges(); + resolve(); + } catch (ex) { + reject(ex); + } + }, 1000); + disposables.push({ dispose: () => clearTimeout(timeout) }); + }); + }); + + + // test('Serialize', async () => { + // const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + // markdownCell.metadata = { + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // }, + // id: '123', + // metadata: { + // foo: 'bar' + // } + // }; + + // const cellMetadata = getCellMetadata({ cell: markdownCell }); + // assert.deepStrictEqual(cellMetadata, { + // id: '123', + // metadata: { + // foo: 'bar', + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // } + // }); + + // const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); + // markdownCell2.metadata = { + // id: '123', + // metadata: { + // foo: 'bar' + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // } + // }; + + // const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell); + // const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2); + // assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2); + + // assert.deepStrictEqual(nbMarkdownCell, { + // cell_type: 'markdown', + // source: ['# header1'], + // metadata: { + // foo: 'bar', + // }, + // attachments: { + // 'image.png': { + // 'image/png': 'abc' + // } + // }, + // id: '123' + // }); + // }); + + // suite('Outputs', () => { + // function validateCellOutputTranslation( + // outputs: nbformat.IOutput[], + // expectedOutputs: vscode.NotebookCellOutput[], + // propertiesToExcludeFromComparison: string[] = [] + // ) { + // const cells: nbformat.ICell[] = [ + // { + // cell_type: 'code', + // execution_count: 10, + // outputs, + // source: 'print(1)', + // metadata: {} + // } + // ]; + // const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + // // OutputItems contain an `id` property generated by VSC. + // // Exclude that property when comparing. + // const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); + // const actualOuts = notebook.cells[0].outputs; + // deepStripProperties(actualOuts, propertiesToExclude); + // deepStripProperties(expectedOutputs, propertiesToExclude); + // assert.deepStrictEqual(actualOuts, expectedOutputs); + // } + + // test('Empty output', () => { + // validateCellOutputTranslation([], []); + // }); + + // test('Stream output', () => { + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stderr', + // text: 'Error' + // }, + // { + // output_type: 'stream', + // name: 'stdout', + // text: 'NoError' + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { + // outputType: 'stream' + // }), + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + // test('Stream output and line endings', () => { + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stdout', + // text: [ + // 'Line1\n', + // '\n', + // 'Line3\n', + // 'Line4' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], { + // outputType: 'stream' + // }) + // ] + // ); + // validateCellOutputTranslation( + // [ + // { + // output_type: 'stream', + // name: 'stdout', + // text: [ + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n', + // 'Hello\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + // test('Multi-line Stream output', () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stdout', + // output_type: 'stream', + // text: [ + // 'Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n'].join(''))], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // output_type: 'stream', + // text: [ + // 'Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n' + // ] + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n', + // '...\n', + // 'Epoch 2/5\n', + // '...\n', + // 'Epoch 3/5\n', + // '...\n', + // 'Epoch 4/5\n', + // '...\n', + // 'Epoch 5/5\n', + // '...\n', + // // This last empty line should not be saved in ipynb. + // '\n'].join(''))], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Streamed text with Ansi characters', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + // { + // outputType: 'stream' + // } + // ) + // ] + // ); + // }); + + // test('Streamed text with angle bracket characters', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '1 is < 2', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { + // outputType: 'stream' + // }) + // ] + // ); + // }); + + // test('Streamed text with angle bracket characters and ansi chars', async () => { + // validateCellOutputTranslation( + // [ + // { + // name: 'stderr', + // text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + // output_type: 'stream' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + // { + // outputType: 'stream' + // } + // ) + // ] + // ); + // }); + + // test('Error', async () => { + // validateCellOutputTranslation( + // [ + // { + // ename: 'Error Name', + // evalue: 'Error Value', + // traceback: ['stack1', 'stack2', 'stack3'], + // output_type: 'error' + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [ + // vscode.NotebookCellOutputItem.error({ + // name: 'Error Name', + // message: 'Error Value', + // stack: ['stack1', 'stack2', 'stack3'].join('\n') + // }) + // ], + // { + // outputType: 'error', + // originalError: { + // ename: 'Error Name', + // evalue: 'Error Value', + // traceback: ['stack1', 'stack2', 'stack3'], + // output_type: 'error' + // } + // } + // ) + // ] + // ); + // }); + + // ['display_data', 'execute_result'].forEach(output_type => { + // suite(`Rich output for output_type = ${output_type}`, () => { + // // Properties to exclude when comparing. + // let propertiesToExcludeFromComparison: string[] = []; + // setup(() => { + // if (output_type === 'display_data') { + // // With display_data the execution_count property will never exist in the output. + // // We can ignore that (as it will never exist). + // // But we leave it in the case of `output_type === 'execute_result'` + // propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; + // } + // }); + + // test('Text mimeType output', async () => { + // validateCellOutputTranslation( + // [ + // { + // data: { + // 'text/plain': 'Hello World!' + // }, + // output_type, + // metadata: {}, + // execution_count: 1 + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + // { + // outputType: output_type, + // metadata: {}, // display_data & execute_result always have metadata. + // executionCount: 1 + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png,jpeg images', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage, + // 'image/jpeg': base64EncodedImage + // }, + // metadata: {}, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [ + // new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), + // new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') + // ], + // { + // executionCount: 1, + // outputType: output_type, + // metadata: {} // display_data & execute_result always have metadata. + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with a light background', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // needs_background: 'light' + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // needs_background: 'light' + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with a dark background', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // needs_background: 'dark' + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // needs_background: 'dark' + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png image with custom dimensions', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // 'image/png': { height: '111px', width: '999px' } + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // 'image/png': { height: '111px', width: '999px' } + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + + // test('png allowed to scroll', async () => { + // validateCellOutputTranslation( + // [ + // { + // execution_count: 1, + // data: { + // 'image/png': base64EncodedImage + // }, + // metadata: { + // unconfined: true, + // 'image/png': { width: '999px' } + // }, + // output_type + // } + // ], + // [ + // new vscode.NotebookCellOutput( + // [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + // { + // executionCount: 1, + // metadata: { + // unconfined: true, + // 'image/png': { width: '999px' } + // }, + // outputType: output_type + // } + // ) + // ], + // propertiesToExcludeFromComparison + // ); + // }); + // }); + // }); + // }); + + // suite('Output Order', () => { + // test('Verify order of outputs', async () => { + // const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [ + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': 'some json', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html'] + // }, + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': 'some json', + // 'application/javascript': 'some js', + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: [ + // 'application/vnd.vegalite.v4+json', + // 'text/html', + // 'application/javascript', + // 'text/plain' + // ] + // }, + // { + // output: { + // data: { + // 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes. + // 'application/javascript': 'some js', + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: [ + // 'text/html', + // 'application/javascript', + // 'text/plain', + // 'application/vnd.vegalite.v4+json' + // ] + // }, + // { + // output: { + // data: { + // 'text/plain': 'some text', + // 'text/html': 'Hello' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['text/html', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'application/javascript': 'some js', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/javascript', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'image/svg+xml': 'some svg', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['image/svg+xml', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'text/latex': 'some latex', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['text/latex', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'application/vnd.jupyter.widget-view+json': 'some widget', + // 'text/plain': 'some text' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain'] + // }, + // { + // output: { + // data: { + // 'text/plain': 'some text', + // 'image/svg+xml': 'some svg', + // 'image/png': 'some png' + // }, + // metadata: {}, + // output_type: 'display_data' + // }, + // expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain'] + // } + // ]; + + // dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => { + // const sortedOutputs = jupyterCellOutputToCellOutput(output); + // const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(','); + // assert.equal(mimeTypes, expectedMimeTypesOrder.join(',')); + // }); + // }); + // }); +}); diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts index cb461539c5d..e132b6b2b1d 100644 --- a/extensions/ipynb/src/test/serializers.test.ts +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -41,6 +41,12 @@ suite(`ipynb serializer`, () => { source: 'print(1)', metadata: {} }, + { + cell_type: 'code', + outputs: [], + source: 'print(2)', + metadata: {} + }, { cell_type: 'markdown', source: '# HEAD', @@ -55,13 +61,18 @@ suite(`ipynb serializer`, () => { expectedCodeCell.metadata = { execution_count: 10, metadata: {} }; expectedCodeCell.executionSummary = { executionOrder: 10 }; + const expectedCodeCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(2)', 'python'); + expectedCodeCell2.outputs = []; + expectedCodeCell2.metadata = { execution_count: null, metadata: {} }; + expectedCodeCell2.executionSummary = {}; + const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); expectedMarkdownCell.outputs = []; expectedMarkdownCell.metadata = { metadata: {} }; - assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); + assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedCodeCell2, expectedMarkdownCell]); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b08b816b95a..b528b21b2a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -282,7 +282,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } get visibleRanges() { - return this._list.visibleRanges || []; + return this._list ? (this._list.visibleRanges || []) : []; } private _baseCellEditorOptions = new Map(); From 70140a270ccb3918a3d2e030ccd9ebbe5664ff99 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 6 Jan 2025 11:15:40 +0100 Subject: [PATCH 06/27] Add a setting to warn when collapsing unsubmitted comments (#236589) Fixes https://github.com/microsoft/vscode-pull-request-github/issues/1455 --- .../comments/browser/commentThreadBody.ts | 5 ++- .../comments/browser/commentThreadHeader.ts | 8 ++-- .../comments/browser/commentThreadWidget.ts | 44 ++++++++++++++----- .../comments/browser/comments.contribution.ts | 6 +++ 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index fa68fe09303..035b88131fe 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -41,7 +41,6 @@ export class CommentThreadBody extends D return this._commentElements.filter(node => node.isEditing)[0]; } - constructor( private readonly _parentEditor: LayoutableEditor, readonly owner: string, @@ -77,6 +76,10 @@ export class CommentThreadBody extends D this._commentsElement.focus(); } + hasCommentsInEditMode() { + return this._commentElements.some(commentNode => commentNode.isEditing); + } + ensureFocusIntoNewEditingComment() { if (this._commentElements.length === 1 && this._commentElements[0].isEditing) { this._commentElements[0].setFocus(true); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 3f42e0cdf32..cb721233760 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -44,9 +44,9 @@ export class CommentThreadHeader extends Disposable { private _delegate: { collapse: () => void }, private _commentMenus: CommentMenus, private _commentThread: languages.CommentThread, - private _contextKeyService: IContextKeyService, - private instantiationService: IInstantiationService, - private _contextMenuService: IContextMenuService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService ) { super(); this._headElement = dom.$('.head'); @@ -63,7 +63,7 @@ export class CommentThreadHeader extends Disposable { const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) + actionViewItemProvider: createActionViewItem.bind(undefined, this._instantiationService) }); this._register(this._actionbarWidget); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 94578614d7b..41fbd153cfd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,6 +5,7 @@ import './media/review.css'; import * as dom from '../../../../base/browser/dom.js'; +import * as nls from '../../../../nls.js'; import * as domStylesheets from '../../../../base/browser/domStylesheets.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -28,7 +29,6 @@ import { IRange, Range } from '../../../../editor/common/core/range.js'; import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from './commentColors.js'; import { ICellRange } from '../../notebook/common/notebookRange.js'; import { FontInfo } from '../../../../editor/common/config/fontInfo.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js'; @@ -38,6 +38,8 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { LayoutableEditor } from './simpleCommentEditor.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import Severity from '../../../../base/common/severity.js'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; @@ -76,10 +78,11 @@ export class CommentThreadWidget extends actionRunner: (() => void) | null; collapse: () => void; }, - @ICommentService private commentService: ICommentService, - @IContextMenuService contextMenuService: IContextMenuService, - @IConfigurationService private configurationService: IConfigurationService, - @IKeybindingService private _keybindingService: IKeybindingService + @ICommentService private readonly commentService: ICommentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IDialogService private readonly _dialogService: IDialogService ) { super(); @@ -89,16 +92,14 @@ export class CommentThreadWidget extends this._commentMenus = this.commentService.getCommentMenus(this._owner); - this._register(this._header = new CommentThreadHeader( + this._register(this._header = this._scopedInstantiationService.createInstance( + CommentThreadHeader, container, { - collapse: this.collapse.bind(this) + collapse: this.collapseAction.bind(this) }, this._commentMenus, - this._commentThread, - this._contextKeyService, - this._scopedInstantiationService, - contextMenuService + this._commentThread )); this._header.updateCommentThread(this._commentThread); @@ -159,6 +160,21 @@ export class CommentThreadWidget extends this.currentThreadListeners(); } + private async confirmCollapse(): Promise { + const confirmSetting = this._configurationService.getValue<'whenHasUnsubmittedComments' | 'never'>('comments.thread.confirmOnCollapse'); + + const hasUnsubmitted = !!this._commentReply?.commentEditor.getValue() || this._body.hasCommentsInEditMode(); + if (confirmSetting === 'whenHasUnsubmittedComments' && hasUnsubmitted) { + const result = await this._dialogService.confirm({ + message: nls.localize('confirmCollapse', "This comment thread has unsubmitted comments. Do you want to collapse it?"), + primaryButton: nls.localize('collapse', "Collapse"), + type: Severity.Warning + }); + return result.confirmed; + } + return true; + } + private _setAriaLabel(): void { let ariaLabel = localize('commentLabel', "Comment"); let keybinding: string | undefined; @@ -375,6 +391,12 @@ export class CommentThreadWidget extends } } + private async collapseAction() { + if (await this.confirmCollapse()) { + this.collapse(); + } + } + collapse() { if (Range.isIRange(this.commentThread.range) && isCodeEditor(this._parentEditor)) { this._parentEditor.setSelection(this.commentThread.range); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index d2cfe1a5680..3cf98a0cde9 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -138,6 +138,12 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', default: true, description: nls.localize('collapseOnResolve', "Controls whether the comment thread should collapse when the thread is resolved.") + }, + 'comments.thread.confirmOnCollapse': { + type: 'string', + enum: ['whenHasUnsubmittedComments', 'never'], + default: 'never', + description: nls.localize('confirmOnCollapse', "Controls whether a confirmation dialog is shown when collapsing a comment thread with unsubmitted comments.") } } }); From e8b620fc968e78136c3417aab4ef1cc9950b55ed Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 11:25:43 +0100 Subject: [PATCH 07/27] Settings progress bar is too eager to appear (fix #200547) (#237322) --- src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 8e3388a7008..43214c91777 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1606,7 +1606,7 @@ export class SettingsEditor2 extends EditorPane { } private async triggerSearch(query: string): Promise { - const progressRunner = this.editorProgressService.show(true); + const progressRunner = this.editorProgressService.show(true, 800); this.viewState.tagFilters = new Set(); this.viewState.extensionFilters = new Set(); this.viewState.featureFilters = new Set(); From dc1cfc044d68746517d5fefd976eb7d13781b170 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 11:27:00 +0100 Subject: [PATCH 08/27] Setup: allow a command link that opens setup view (fix microsoft/vscode-copilot#11301) (#237323) --- .../contrib/chat/browser/chatSetup.ts | 18 ++++++++++- .../extensions/browser/extensionUrlHandler.ts | 30 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 53a86ec5245..12567365126 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -60,6 +60,7 @@ import { IHostService } from '../../../services/host/browser/host.js'; import Severity from '../../../../base/common/severity.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; +import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -109,7 +110,9 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr constructor( @IProductService private readonly productService: IProductService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ICommandService private readonly commandService: ICommandService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); @@ -122,6 +125,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr this.registerChatWelcome(); this.registerActions(); + this.registerUrlLinkHandler(); } private registerChatWelcome(): void { @@ -292,6 +296,18 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr registerAction2(ChatSetupHideAction); registerAction2(UpgradePlanAction); } + + private registerUrlLinkHandler(): void { + this._register(ExtensionUrlHandlerOverrideRegistry.registerHandler(URI.parse(`${this.productService.urlProtocol}://${defaultChat.chatExtensionId}`), { + handleURL: async () => { + this.telemetryService.publicLog2('workbenchActionExecuted', { id: TRIGGER_SETUP_COMMAND_ID, from: 'url' }); + + await this.commandService.executeCommand(TRIGGER_SETUP_COMMAND_ID); + + return true; + } + })); + } } //#endregion diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index ac3c7ed5006..04aa6fe51f8 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize, localize2 } from '../../../../nls.js'; -import { IDisposable, combinedDisposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -27,6 +27,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { isCancellationError } from '../../../../base/common/errors.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; +import { ResourceMap } from '../../../../base/common/map.js'; const FIVE_MINUTES = 5 * 60 * 1000; const THIRTY_SECONDS = 30 * 1000; @@ -99,6 +100,25 @@ type ExtensionUrlReloadHandlerClassification = { comment: 'This is used to understand the drop funnel of extension URI handling by the OS & VS Code.'; }; +export interface IExtensionUrlHandlerOverride { + handleURL(uri: URI): Promise; +} + +export class ExtensionUrlHandlerOverrideRegistry { + + private static readonly handlers = new ResourceMap(); + + static registerHandler(uri: URI, handler: IExtensionUrlHandlerOverride): IDisposable { + this.handlers.set(uri, handler); + + return toDisposable(() => this.handlers.delete(uri)); + } + + static getHandler(uri: URI): IExtensionUrlHandlerOverride | undefined { + return this.handlers.get(uri); + } +} + /** * This class handles URLs which are directed towards extensions. * If a URL is directed towards an inactive extension, it buffers it, @@ -153,6 +173,14 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { return false; } + const overrideHandler = ExtensionUrlHandlerOverrideRegistry.getHandler(uri); + if (overrideHandler) { + const handled = await overrideHandler.handleURL(uri); + if (handled) { + return handled; + } + } + const extensionId = uri.authority; this.telemetryService.publicLog2('uri_invoked/start', { extensionId }); From 567779ca5154518823ac35f2cdb0efa36fe1cba7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 14:34:23 +0100 Subject: [PATCH 09/27] Status bar tooltip - add support for specifying a list of commands and lazy contents (#234339) (#237336) Core changes. --- .../browser/parts/statusbar/statusbarItem.ts | 27 ++++++++++++++----- .../services/statusbar/browser/statusbar.ts | 20 ++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 3de50cb8937..149ee4cbff5 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -8,7 +8,7 @@ import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IStatusbarEntry, ShowTooltipCommand, StatusbarEntryKinds } from '../../../services/statusbar/browser/statusbar.js'; +import { IStatusbarEntry, isTooltipWithCommands, ShowTooltipCommand, StatusbarEntryKinds, TooltipContent } from '../../../services/statusbar/browser/statusbar.js'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeColor } from '../../../../base/common/themables.js'; @@ -24,7 +24,7 @@ import { spinningLoading, syncing } from '../../../../platform/theme/common/icon import { isMarkdownString, markdownStringEqual } from '../../../../base/common/htmlContent.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; -import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; +import { IManagedHover, IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export class StatusbarEntryItem extends Disposable { @@ -116,11 +116,26 @@ export class StatusbarEntryItem extends Disposable { // Update: Hover if (!this.entry || !this.isEqualTooltip(this.entry, entry)) { - const hoverContents = isMarkdownString(entry.tooltip) ? { markdown: entry.tooltip, markdownNotSupportedFallback: undefined } : entry.tooltip; - if (this.hover) { - this.hover.update(hoverContents); + let hoverOptions: IManagedHoverOptions | undefined; + let hoverTooltip: TooltipContent | undefined; + if (isTooltipWithCommands(entry.tooltip)) { + hoverTooltip = entry.tooltip.content; + hoverOptions = { + actions: entry.tooltip.commands.map(command => ({ + commandId: command.id, + label: command.title, + run: () => this.executeCommand(command) + })) + }; } else { - this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents)); + hoverTooltip = entry.tooltip; + } + + const hoverContents = isMarkdownString(hoverTooltip) ? { markdown: hoverTooltip, markdownNotSupportedFallback: undefined } : hoverTooltip; + if (this.hover) { + this.hover.update(hoverContents, hoverOptions); + } else { + this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents, hoverOptions)); } } diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 3b3f1f1fe17..7c81333e416 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -8,6 +8,7 @@ import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle. import { ThemeColor } from '../../../../base/common/themables.js'; import { Command } from '../../../../editor/common/languages.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { ColorIdentifier } from '../../../../platform/theme/common/colorRegistry.js'; import { IAuxiliaryStatusbarPart, IStatusbarEntryContainer } from '../../../browser/parts/statusbar/statusbarPart.js'; @@ -111,6 +112,19 @@ export interface IStatusbarStyleOverride { export type StatusbarEntryKind = 'standard' | 'warning' | 'error' | 'prominent' | 'remote' | 'offline'; export const StatusbarEntryKinds: StatusbarEntryKind[] = ['standard', 'warning', 'error', 'prominent', 'remote', 'offline']; +export type TooltipContent = string | IMarkdownString | IManagedHoverTooltipMarkdownString | HTMLElement; + +export interface ITooltipWithCommands { + readonly content: TooltipContent; + readonly commands: Command[]; +} + +export function isTooltipWithCommands(thing: unknown): thing is ITooltipWithCommands { + const candidate = thing as ITooltipWithCommands | undefined; + + return !!candidate?.content && Array.isArray(candidate?.commands); +} + /** * A declarative way of describing a status bar entry */ @@ -141,9 +155,11 @@ export interface IStatusbarEntry { readonly role?: string; /** - * An optional tooltip text to show when you hover over the entry + * An optional tooltip text to show when you hover over the entry. + * + * Use `ITooltipWithCommands` to show a tooltip with commands in hover footer area. */ - readonly tooltip?: string | IMarkdownString | HTMLElement; + readonly tooltip?: TooltipContent | ITooltipWithCommands; /** * An optional color to use for the entry. From b7f437d2a1974e160f3ec00b9e72526d27bc4fb9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 15:37:07 +0100 Subject: [PATCH 10/27] Allow custom titlebar on Linux as experiment (microsoft/vscode-internalbacklog#4857) (#237337) --- src/vs/code/electron-main/app.ts | 12 +++++-- src/vs/platform/native/common/native.ts | 9 +++-- .../electron-main/nativeHostMainService.ts | 16 +++++++-- src/vs/platform/window/common/window.ts | 20 ++++++++++- .../electron-sandbox/desktop.contribution.ts | 1 + .../parts/titlebar/titlebarPart.ts | 34 +++++++++++++++++-- .../electron-sandbox/workbenchTestServices.ts | 1 + 7 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 11ca66e8767..7c7fc636e21 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -83,7 +83,7 @@ import { NativeURLService } from '../../platform/url/common/urlService.js'; import { ElectronURLListener } from '../../platform/url/electron-main/electronUrlListener.js'; import { IWebviewManagerService } from '../../platform/webview/common/webviewManagerService.js'; import { WebviewMainService } from '../../platform/webview/electron-main/webviewMainService.js'; -import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable } from '../../platform/window/common/window.js'; +import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, TitlebarStyle, overrideDefaultTitlebarStyle } from '../../platform/window/common/window.js'; import { IWindowsMainService, OpenContext } from '../../platform/windows/electron-main/windows.js'; import { ICodeWindow } from '../../platform/window/electron-main/window.js'; import { WindowsMainService } from '../../platform/windows/electron-main/windowsMainService.js'; @@ -593,6 +593,14 @@ export class CodeApplication extends Disposable { // Services const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); + // Linux (stable only): custom title default style override + if (isLinux && this.productService.quality === 'stable') { + const titleBarDefaultStyleOverride = this.stateService.getItem('window.titleBarStyleOverride'); + if (titleBarDefaultStyleOverride === TitlebarStyle.CUSTOM || titleBarDefaultStyleOverride === TitlebarStyle.NATIVE) { + overrideDefaultTitlebarStyle(titleBarDefaultStyleOverride); + } + } + // Auth Handler appInstantiationService.invokeFunction(accessor => accessor.get(IProxyAuthService)); @@ -605,7 +613,7 @@ export class CodeApplication extends Disposable { // Setup Protocol URL Handlers const initialProtocolUrls = await appInstantiationService.invokeFunction(accessor => this.setupProtocolUrlHandlers(accessor, mainProcessElectronServer)); - // Setup vscode-remote-resource protocol handler. + // Setup vscode-remote-resource protocol handler this.setupManagedRemoteResourceUrlHandler(mainProcessElectronServer); // Signal phase: ready - before opening first window diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 2fdfb63e0b5..1f8d0adabe7 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -113,6 +113,9 @@ export interface ICommonNativeHostService { */ focusWindow(options?: INativeHostOptions & { force?: boolean }): Promise; + // Titlebar default style override + overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): Promise; + // Dialogs showMessageBox(options: MessageBoxOptions & INativeHostOptions): Promise; showSaveDialog(options: SaveDialogOptions & INativeHostOptions): Promise; @@ -143,10 +146,6 @@ export interface ICommonNativeHostService { hasWSLFeatureInstalled(): Promise; // Screenshots - - /** - * Gets a screenshot of the currently active Electron window. - */ getScreenshot(): Promise; // Process @@ -199,7 +198,7 @@ export interface ICommonNativeHostService { loadCertificates(): Promise; findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; - // Registry (windows only) + // Registry (Windows only) windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise; } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 794c5aed575..8e5c129ea4f 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -33,7 +33,7 @@ import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { defaultWindowState, ICodeWindow } from '../../window/electron-main/window.js'; -import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from '../../window/common/window.js'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable, overrideDefaultTitlebarStyle } from '../../window/common/window.js'; import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from '../../workspace/common/workspace.js'; import { IWorkspacesManagementMainService } from '../../workspaces/electron-main/workspacesManagementMainService.js'; @@ -48,6 +48,7 @@ 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 { IStateService } from '../../state/node/state.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -70,7 +71,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IConfigurationService private readonly configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, @IProxyAuthService private readonly proxyAuthService: IProxyAuthService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStateService private readonly stateService: IStateService ) { super(); } @@ -324,6 +326,15 @@ export class NativeHostMainService extends Disposable implements INativeHostMain this.themeMainService.saveWindowSplash(windowId, splash); } + async overrideDefaultTitlebarStyle(windowId: number | undefined, style: 'native' | 'custom' | undefined): Promise { + if (typeof style === 'string') { + this.stateService.setItem('window.titleBarStyleOverride', style); + } else { + this.stateService.removeItem('window.titleBarStyleOverride'); + } + overrideDefaultTitlebarStyle(style); + } + //#endregion @@ -697,6 +708,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); const captured = await window?.win?.webContents.capturePage(); + return captured?.toJPEG(95); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 3a03481e55a..02e4152376d 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -187,10 +187,23 @@ export const enum CustomTitleBarVisibility { NEVER = 'never', } +export let titlebarStyleDefaultOverride: TitlebarStyle | undefined = undefined; +export function overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): void { + switch (style) { + case 'native': + titlebarStyleDefaultOverride = TitlebarStyle.NATIVE; + break; + case 'custom': + titlebarStyleDefaultOverride = TitlebarStyle.CUSTOM; + break; + default: + titlebarStyleDefaultOverride = undefined; + } +} + export function hasCustomTitlebar(configurationService: IConfigurationService, titleBarStyle?: TitlebarStyle): boolean { // Returns if it possible to have a custom title bar in the curren session // Does not imply that the title bar is visible - return true; } @@ -198,6 +211,7 @@ export function hasNativeTitlebar(configurationService: IConfigurationService, t if (!titleBarStyle) { titleBarStyle = getTitleBarStyle(configurationService); } + return titleBarStyle === TitlebarStyle.NATIVE; } @@ -224,6 +238,10 @@ export function getTitleBarStyle(configurationService: IConfigurationService): T } } + if (titlebarStyleDefaultOverride) { + return titlebarStyleDefaultOverride; + } + return isLinux && product.quality === 'stable' ? TitlebarStyle.NATIVE : TitlebarStyle.CUSTOM; // default to custom on all OS except Linux stable (for now) } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 19a8cf11e84..da72519dee3 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -234,6 +234,7 @@ import product from '../../platform/product/common/product.js'; 'type': 'string', 'enum': ['native', 'custom'], 'default': isLinux && product.quality === 'stable' ? 'native' : 'custom', + 'tags': isLinux && product.quality === 'stable' ? ['onExP'] : undefined, 'scope': ConfigurationScope.APPLICATION, 'description': localize('titleBarStyle', "Adjust the appearance of the window title bar to be native by the OS or custom. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply."), }, diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 04095aab5d7..0d7a06de173 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/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 { IProductService } from '../../../../platform/product/common/productService.js'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -70,7 +71,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, - @INativeHostService private readonly nativeHostService: INativeHostService, + @INativeHostService protected readonly nativeHostService: INativeHostService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, @@ -287,9 +288,38 @@ export class MainNativeTitlebarPart extends NativeTitlebarPart { @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IMenuService menuService: IMenuService, - @IKeybindingService keybindingService: IKeybindingService + @IKeybindingService keybindingService: IKeybindingService, + @IProductService productService: IProductService ) { super(Parts.TITLEBAR_PART, mainWindow, 'main', contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService); + + if (isLinux && productService.quality === 'stable') { + this.handleDefaultTitlebarStyle(); // TODO@bpasero remove me eventually once settled + } + } + + private handleDefaultTitlebarStyle(): void { + this.updateDefaultTitlebarStyle(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('window.titleBarStyle')) { + this.updateDefaultTitlebarStyle(); + } + })); + } + + private updateDefaultTitlebarStyle(): void { + const titleBarStyle = this.configurationService.inspect('window.titleBarStyle'); + + let titleBarStyleOverride: 'custom' | undefined; + if (titleBarStyle.applicationValue || titleBarStyle.userValue || titleBarStyle.userLocalValue) { + // configured by user or application: clear override + titleBarStyleOverride = undefined; + } else { + // not configured: set override if experiment is active + titleBarStyleOverride = titleBarStyle.defaultValue === 'native' ? undefined : 'custom'; + } + + this.nativeHostService.overrideDefaultTitlebarStyle(titleBarStyleOverride); } } diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 9bc958141ff..ecec6ce983d 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -162,6 +162,7 @@ export class TestNativeHostService implements INativeHostService { 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 overrideDefaultTitlebarStyle(style: 'native' | 'custom' | undefined): Promise { } } export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { From d0dfb42e3468b5b0aa87bd825ec5da20ac1684d2 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 7 Jan 2025 00:33:12 +0900 Subject: [PATCH 11/27] ci: fix broken monaco editor action (#237347) --- .github/workflows/monaco-editor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index 426999ce43b..1f5694faec2 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -45,11 +45,11 @@ jobs: path: ${{ steps.npmCacheDirPath.outputs.dir }} key: ${{ runner.os }}-npmCacheDir-${{ steps.nodeModulesCacheKey.outputs.value }} restore-keys: ${{ runner.os }}-npmCacheDir- - - name: Install libkrb5-dev + - name: Install system dependencies if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} run: | sudo apt update - sudo apt install -y libkrb5-dev + sudo apt install -y libxkbfile-dev pkg-config libkrb5-dev libxss1 - name: Execute npm if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} env: From aaa576acca01852119f6a6b0260cf5aa74a30c58 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 16:54:06 +0100 Subject: [PATCH 12/27] Add time information to chatinstallentitlement event (fix microsoft/vscode-copilot#11476) (#237350) --- src/vs/workbench/contrib/chat/browser/chatSetup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 12567365126..52bb5bea1b0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -318,6 +318,7 @@ type EntitlementClassification = { 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' }; + quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; owner: 'bpasero'; comment: 'Reporting chat setup entitlements'; }; @@ -326,6 +327,7 @@ type EntitlementEvent = { entitlement: ChatEntitlement; quotaChat: number | undefined; quotaCompletions: number | undefined; + quotaResetDate: string | undefined; }; interface IEntitlementsResponse { @@ -536,7 +538,8 @@ class ChatSetupRequests extends Disposable { this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, quotaChat: entitlementsResponse.limited_user_quotas?.chat, - quotaCompletions: entitlementsResponse.limited_user_quotas?.completions + quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, + quotaResetDate: entitlementsResponse.limited_user_reset_date }); return entitlements; From 4e2ba23aed553047bd7b5726f1b92520adeda749 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 16:59:10 +0100 Subject: [PATCH 13/27] chat setup - restore copilot menu on extension install (#237348) --- .../browser/actions/chatGettingStarted.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 8dc7d4dee91..ea35e44e016 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -16,6 +16,7 @@ import { IDefaultChatAgent } from '../../../../../base/common/product.js'; import { IViewDescriptorService } from '../../../../common/views.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; import { ensureSideBarChatViewSize } from '../chat.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -32,6 +33,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb @IStorageService private readonly storageService: IStorageService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); @@ -60,14 +62,25 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb if (ExtensionIdentifier.equals(defaultChatAgent.extensionId, ext.value)) { const extensionStatus = this.extensionService.getExtensionsStatus(); if (extensionStatus[ext.value].activationTimes && this.recentlyInstalled) { - await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); - ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); - this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - this.recentlyInstalled = false; + this.onDidInstallChat(); return; } } } })); } + + private async onDidInstallChat() { + + // Enable chat command center if previously disabled + this.configurationService.updateValue('chat.commandCenter.enabled', true); + + // Open and configure chat view + await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); + ensureSideBarChatViewSize(400, this.viewDescriptorService, this.layoutService); + + // Only do this once + this.storageService.store(ChatGettingStartedContribution.hideWelcomeView, true, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.recentlyInstalled = false; + } } From 3548eae0e119c7bdb948d138be0c129dad71493f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:05:37 +0100 Subject: [PATCH 14/27] Git - add `git.commitShortHashLength` setting (#237343) --- extensions/git/package.json | 8 ++++++++ extensions/git/package.nls.json | 5 +++-- extensions/git/src/blame.ts | 19 +++++++++++-------- extensions/git/src/commands.ts | 15 +++++++++------ extensions/git/src/historyProvider.ts | 22 ++++++++++++---------- extensions/git/src/repository.ts | 6 +++--- extensions/git/src/util.ts | 8 +++++++- 7 files changed, 53 insertions(+), 30 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index cdb7c77a03a..064dfe3f1cf 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3229,6 +3229,14 @@ "type": "string", "default": "${authorName} (${authorDateAgo})", "markdownDescription": "%config.blameStatusBarItem.template%" + }, + "git.commitShortHashLength": { + "type": "number", + "default": 7, + "minimum": 7, + "maximum": 40, + "markdownDescription": "%config.commitShortHashLength%", + "scope": "resource" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6db253137e1..afbb44ba48f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -278,9 +278,10 @@ "config.publishBeforeContinueOn.prompt": "Prompt to publish unpublished Git state when using Continue Working On from a Git repository", "config.similarityThreshold": "Controls the threshold of the similarity index (the amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename. **Note:** Requires Git version `2.18.0` or later.", "config.blameEditorDecoration.enabled": "Controls whether to show blame information in the editor using editor decorations.", - "config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First 8 characters of the commit hash\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.blameEditorDecoration.template": "Template for the blame information editor decoration. 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.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 8 characters of the commit hash\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.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.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 3c7d61dd1e1..0dbdca92b32 100644 --- a/extensions/git/src/blame.ts +++ b/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, IDisposable } from './util'; +import { dispose, fromNow, getCommitShortHash, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation, Commit } from './git'; @@ -186,14 +186,14 @@ export class GitBlameController { this._onDidChangeConfiguration(); } - formatBlameInformationMessage(template: string, blameInformation: BlameInformation): string { + 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: blameInformation.hash.substring(0, 8), + hashShort: getCommitShortHash(documentUri, blameInformation.hash), subject: emojify(subject ?? ''), authorName: blameInformation.authorName ?? '', authorEmail: blameInformation.authorEmail ?? '', @@ -260,7 +260,7 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${blameInformationOrCommit.hash.substring(0, 8)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); @@ -571,7 +571,9 @@ class GitBlameEditorDecoration implements HoverProvider { } private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { - if (e && !e.affectsConfiguration('git.blame.editorDecoration.template')) { + if (e && + !e.affectsConfiguration('git.commitShortHashLength') && + !e.affectsConfiguration('git.blame.editorDecoration.template')) { return; } @@ -610,7 +612,7 @@ class GitBlameEditorDecoration implements HoverProvider { const decorations = blameInformation.map(blame => { const contentText = typeof blame.blameInformation !== 'string' - ? this._controller.formatBlameInformationMessage(template, blame.blameInformation) + ? this._controller.formatBlameInformationMessage(textEditor.document.uri, template, blame.blameInformation) : blame.blameInformation; return this._createDecoration(blame.lineNumber, contentText); @@ -663,7 +665,8 @@ class GitBlameStatusBarItem { } private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.statusBarItem.template')) { + if (!e.affectsConfiguration('git.commitShortHashLength') && + !e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } @@ -690,7 +693,7 @@ class GitBlameStatusBarItem { const config = workspace.getConfiguration('git'); const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); - this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(template, blameInformation[0].blameInformation)}`; + this._statusBarItem.text = `$(git-commit) ${this._controller.formatBlameInformationMessage(window.activeTextEditor.document.uri, template, blameInformation[0].blameInformation)}`; this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 95ab9ada071..f7476a23214 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -14,7 +14,7 @@ import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { dispose, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; +import { dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -4286,16 +4286,17 @@ export class CommandCenter { let title: string | undefined; let historyItemParentId: string | undefined; + const rootUri = Uri.file(repository.root); // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is // provided, we are viewing a range and we have to include both start and end commits. // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository if (!historyItem2) { const commit = await repository.getCommit(historyItem1.id); - title = `${historyItem1.id.substring(0, 8)} - ${truncate(commit.message)}`; + title = `${getCommitShortHash(rootUri, historyItem1.id)} - ${truncate(commit.message)}`; historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; } else { - title = l10n.t('All Changes ({0} ↔ {1})', historyItem2.id.substring(0, 8), historyItem1.id.substring(0, 8)); + title = l10n.t('All Changes ({0} ↔ {1})', getCommitShortHash(rootUri, historyItem2.id), getCommitShortHash(rootUri, historyItem1.id)); historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; } @@ -4310,8 +4311,9 @@ export class CommandCenter { return; } - const modifiedShortRef = historyItem.id.substring(0, 8); - const originalShortRef = historyItem.parentIds.length > 0 ? historyItem.parentIds[0].substring(0, 8) : `${modifiedShortRef}^`; + const rootUri = Uri.file(repository.root); + const modifiedShortRef = getCommitShortHash(rootUri, historyItem.id); + const originalShortRef = historyItem.parentIds.length > 0 ? getCommitShortHash(rootUri, historyItem.parentIds[0]) : `${modifiedShortRef}^`; const title = l10n.t('All Changes ({0} ↔ {1})', originalShortRef, modifiedShortRef); const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); @@ -4350,8 +4352,9 @@ export class CommandCenter { return; } + const rootUri = Uri.file(repository.root); const commit = await repository.getCommit(historyItemId); - const title = `${historyItemId.substring(0, 8)} - ${truncate(commit.message)}`; + const title = `${getCommitShortHash(rootUri, historyItemId)} - ${truncate(commit.message)}`; const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` }); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 896c46851c4..250cf80560d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -6,20 +6,22 @@ import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, getCommitShortHash } from './util'; import { toGitUri } from './uri'; import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; -function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { +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}', ref.commit.substring(0, 8)) : undefined, + 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') @@ -28,7 +30,7 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { return { id: `refs/tags/${ref.name}`, name: ref.name ?? '', - description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + description: ref.commit ? l10n.t('Tag at {0}', getCommitShortHash(rootUri, ref.commit)) : undefined, revision: ref.commit, icon: new ThemeIcon('tag'), category: l10n.t('tags') @@ -37,7 +39,7 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { return { id: `refs/heads/${ref.name}`, name: ref.name ?? '', - description: ref.commit ? ref.commit.substring(0, 8) : undefined, + description: ref.commit ? getCommitShortHash(rootUri, ref.commit) : undefined, revision: ref.commit, icon: new ThemeIcon('git-branch'), category: l10n.t('branches') @@ -178,7 +180,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Refs (alphabetically) const historyItemRefs = this.repository.refs - .map(ref => toSourceControlHistoryItemRef(ref)) + .map(ref => toSourceControlHistoryItemRef(this.repository, ref)) .sort((a, b) => a.id.localeCompare(b.id)); // Auto-fetch @@ -207,13 +209,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec for (const ref of refs) { switch (ref.type) { case RefType.RemoteHead: - remoteBranches.push(toSourceControlHistoryItemRef(ref)); + remoteBranches.push(toSourceControlHistoryItemRef(this.repository, ref)); break; case RefType.Tag: - tags.push(toSourceControlHistoryItemRef(ref)); + tags.push(toSourceControlHistoryItemRef(this.repository, ref)); break; default: - branches.push(toSourceControlHistoryItemRef(ref)); + branches.push(toSourceControlHistoryItemRef(this.repository, ref)); break; } } @@ -259,7 +261,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec message: emojify(commit.message), author: commit.authorName, icon: new ThemeIcon('git-commit'), - displayId: commit.hash.substring(0, 8), + displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, references: references.length !== 0 ? references : undefined diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4bf7fa32ef0..e1b71d87f0a 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -23,7 +23,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; @@ -1657,7 +1657,7 @@ export class Repository implements Disposable { } async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise { - const refLabel = opts?.detached ? treeish.substring(0, 8) : treeish; + const refLabel = opts?.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish; await this.run(Operation.Checkout(refLabel), async () => { @@ -1675,7 +1675,7 @@ export class Repository implements Disposable { } async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise { - const refLabel = opts.detached ? treeish.substring(0, 8) : treeish; + const refLabel = opts.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish; await this.run(Operation.CheckoutTracking(refLabel), () => this.repository.checkout(treeish, [], { ...opts, track: true })); } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 8fb85493a9b..759ccdf82de 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -766,3 +766,9 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } } } + +export function getCommitShortHash(scope: Uri, hash: string): string { + const config = workspace.getConfiguration('git', scope); + const shortHashLength = config.get('commitShortHashLength', 7); + return hash.substring(0, shortHashLength); +} From e3bea63a7a854fc5e912c1cb05d3390c85bc38f9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 6 Jan 2025 17:14:40 +0100 Subject: [PATCH 15/27] clean up in extensions scanning (#237344) --- .../common/extensionsProfileScannerService.ts | 6 +- .../common/extensionsScannerService.ts | 88 ++++++++++--------- .../node/extensionManagementService.ts | 73 +++++---------- .../extensionsProfileScannerService.test.ts | 31 ++++--- .../node/extensionsScannerService.test.ts | 56 ++++++------ src/vs/server/node/remoteExtensionsScanner.ts | 2 +- .../cachedExtensionScanner.ts | 4 +- 7 files changed, 122 insertions(+), 138 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index ced131827be..162f11b5c96 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -85,7 +85,7 @@ export interface IExtensionsProfileScannerService { scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; - removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; + removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise; } export abstract class AbstractExtensionsProfileScannerService extends Disposable implements IExtensionsProfileScannerService { @@ -193,13 +193,13 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return updatedExtensions; } - async removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise { + async removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; for (const e of profileExtensions) { - if (areSameExtensions(e.identifier, extension.identifier)) { + if (extensions.some(extension => areSameExtensions(e.identifier, extension))) { extensionsToRemove.push(e); } else { result.push(e); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index a186b6fa045..f852d912536 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -100,16 +100,24 @@ interface IBuiltInExtensionControl { [name: string]: 'marketplace' | 'disabled' | string; } -export type ScanOptions = { - readonly profileLocation?: URI; - readonly includeInvalid?: boolean; - readonly includeAllVersions?: boolean; +export type SystemExtensionsScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; +}; + +export type UserExtensionsScanOptions = { + readonly profileLocation: URI; + readonly includeInvalid?: boolean; + readonly language?: string; readonly useCache?: boolean; readonly productVersion?: IProductVersion; }; +export type ScanOptions = { + readonly includeInvalid?: boolean; + readonly language?: string; +}; + export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); export interface IExtensionsScannerService { readonly _serviceBrand: undefined; @@ -118,17 +126,16 @@ export interface IExtensionsScannerService { readonly userExtensionsLocation: URI; readonly onDidChangeCache: Event; - getTargetPlatform(): Promise; + scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise; + scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise; + scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise; + scanAllUserExtensions(): Promise; - scanAllExtensions(systemScanOptions: ScanOptions, userScanOptions: ScanOptions, includeExtensionsUnderDev: boolean): Promise; - scanSystemExtensions(scanOptions: ScanOptions): Promise; - scanUserExtensions(scanOptions: ScanOptions): Promise; - scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise; + scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise; scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; - scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise; + scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise; - scanMetadata(extensionLocation: URI): Promise; updateMetadata(extensionLocation: URI, metadata: Partial): Promise; initializeDefaultProfileExtensions(): Promise; } @@ -167,35 +174,33 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } private _targetPlatformPromise: Promise | undefined; - getTargetPlatform(): Promise { + private getTargetPlatform(): Promise { if (!this._targetPlatformPromise) { this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); } return this._targetPlatformPromise; } - async scanAllExtensions(systemScanOptions: ScanOptions, userScanOptions: ScanOptions, includeExtensionsUnderDev: boolean): Promise { + async scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise { const [system, user] = await Promise.all([ this.scanSystemExtensions(systemScanOptions), this.scanUserExtensions(userScanOptions), ]); - const development = includeExtensionsUnderDev ? await this.scanExtensionsUnderDevelopment(systemScanOptions, [...system, ...user]) : []; - return this.dedupExtensions(system, user, development, await this.getTargetPlatform(), true); + return this.dedupExtensions(system, user, [], await this.getTargetPlatform(), true); } - async scanSystemExtensions(scanOptions: ScanOptions): Promise { + async scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise { const promises: Promise[] = []; - promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language)); + promises.push(this.scanDefaultSystemExtensions(scanOptions.language)); promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile)); const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); - return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, scanOptions, false); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, { pickLatest: false }); } - async scanUserExtensions(scanOptions: ScanOptions): Promise { - const location = scanOptions.profileLocation ?? this.userExtensionsLocation; - this.logService.trace('Started scanning user extensions', location); + async scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise { + this.logService.trace('Started scanning user extensions', scanOptions.profileLocation); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(scanOptions.profileLocation, true, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -208,16 +213,22 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem throw error; } } - extensions = await this.applyScanOptions(extensions, ExtensionType.User, scanOptions, true); + extensions = await this.applyScanOptions(extensions, ExtensionType.User, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); this.logService.trace('Scanned user extensions:', extensions.length); return extensions; } - async scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise { + async scanAllUserExtensions(scanOptions: { includeAllVersions?: boolean; includeInvalid: boolean } = { includeInvalid: true, includeAllVersions: true }): Promise { + const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, false, ExtensionType.User, undefined, true, undefined, this.getProductVersion()); + const extensions = await this.extensionsScanner.scanExtensions(extensionsScannerInput); + return this.applyScanOptions(extensions, ExtensionType.User, { includeAllVersions: scanOptions.includeAllVersions, includeInvalid: scanOptions.includeInvalid }); + } + + async scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise { if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -227,13 +238,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem }); }))) .flat(); - return this.applyScanOptions(extensions, 'development', scanOptions, true); + return this.applyScanOptions(extensions, 'development', { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } return []; } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,9 +256,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); - return this.applyScanOptions(extensions, extensionType, scanOptions, true); + return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } async scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise { @@ -256,14 +267,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const scannedExtensions = await this.scanOneOrMultipleExtensions(extensionLocation, extensionType, scanOptions); extensions.push(...scannedExtensions); })); - return this.applyScanOptions(extensions, extensionType, scanOptions, true); - } - - async scanMetadata(extensionLocation: URI): Promise { - const manifestLocation = joinPath(extensionLocation, 'package.json'); - const content = (await this.fileService.readFile(manifestLocation)).value.toString(); - const manifest: IScannedExtensionManifest = JSON.parse(content); - return manifest.__metadata; + return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true }); } async updateMetadata(extensionLocation: URI, metaData: Partial): Promise { @@ -301,7 +305,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem this.initializeDefaultProfileExtensionsPromise = (async () => { try { this.logService.info('Started initializing default profile extensions in extensions installation folder.', this.userExtensionsLocation.toString()); - const userExtensions = await this.scanUserExtensions({ includeInvalid: true }); + const userExtensions = await this.scanAllUserExtensions({ includeInvalid: true }); if (userExtensions.length) { await this.extensionsProfileScannerService.addExtensionsToProfile(userExtensions.map(e => [e, e.metadata]), this.userDataProfilesService.defaultProfile.extensionsResource); } else { @@ -324,9 +328,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return this.initializeDefaultProfileExtensionsPromise; } - private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: ScanOptions, pickLatest: boolean): Promise { + private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: { includeAllVersions?: boolean; includeInvalid?: boolean; pickLatest?: boolean } = {}): Promise { if (!scanOptions.includeAllVersions) { - extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), pickLatest); + extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), !!scanOptions.pickLatest); } if (!scanOptions.includeInvalid) { extensions = extensions.filter(extension => extension.isValid); @@ -399,10 +403,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return [...result.values()]; } - private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { + private async scanDefaultSystemExtensions(language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); - const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; + const extensionsScanner = !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); return result; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 762da10967f..e9b38bc69f2 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -17,7 +17,7 @@ import { Schemas } from '../../../base/common/network.js'; import * as path from '../../../base/common/path.js'; import { joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; -import { isBoolean } from '../../../base/common/types.js'; +import { isBoolean, isDefined, isUndefined } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import * as pfs from '../../../base/node/pfs.js'; @@ -37,7 +37,7 @@ import { } from '../common/extensionManagement.js'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js'; -import { IExtensionsScannerService, IScannedExtension, ScanOptions } from '../common/extensionsScannerService.js'; +import { IExtensionsScannerService, IScannedExtension, UserExtensionsScanOptions } from '../common/extensionsScannerService.js'; import { ExtensionsDownloader } from './extensionDownloader.js'; import { ExtensionsLifecycle } from './extensionLifecycle.js'; import { fromExtractError, getManifest } from './extensionManagementUtil.js'; @@ -132,7 +132,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } scanAllUserInstalledExtensions(): Promise { - return this.extensionsScanner.scanAllUserExtensions(false); + return this.extensionsScanner.scanAllUserExtensions(); } scanInstalledExtensionAtLocation(location: URI): Promise { @@ -298,23 +298,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const local = await this.extensionsScanner.extractUserExtension( extensionKey, location.fsPath, - { - id: gallery.identifier.uuid, - publisherId: gallery.publisherId, - publisherDisplayName: gallery.publisherDisplayName, - targetPlatform: gallery.properties.targetPlatform, - isApplicationScoped: options.isApplicationScoped, - isMachineScoped: options.isMachineScoped, - isBuiltin: options.isBuiltin, - isPreReleaseVersion: gallery.properties.isPreReleaseVersion, - hasPreReleaseVersion: gallery.properties.isPreReleaseVersion, - installedTimestamp: Date.now(), - pinned: options.installGivenVersion ? true : !!options.pinned, - preRelease: isBoolean(options.preRelease) - ? options.preRelease - : options.installPreReleaseVersion || gallery.properties.isPreReleaseVersion, - source: 'gallery', - }, false, token); @@ -382,14 +365,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi const local = await this.extensionsScanner.extractUserExtension( extensionKey, path.resolve(location.fsPath), - { - isApplicationScoped: options.isApplicationScoped, - isMachineScoped: options.isMachineScoped, - isBuiltin: options.isBuiltin, - installedTimestamp: Date.now(), - pinned: options.installGivenVersion ? true : !!options.pinned, - source: 'vsix', - }, isBoolean(options.keepExisting) ? !options.keepExisting : true, token); return { local }; @@ -561,17 +536,18 @@ export class ExtensionsScanner extends Disposable { async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); await this.deleteExtensionsMarkedForRemoval(); - await this.initializeMetadata(); + //TODO: Remove this initiialization after coupe of releases + await this.initializeExtensionSize(); } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { try { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; + const userScanOptions: UserExtensionsScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(profileLocation); if (!scanAllExtensionsPromise) { - scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ includeInvalid: true, useCache: true }, userScanOptions, false) + scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({}, userScanOptions) .finally(() => this.scanAllExtensionPromise.delete(profileLocation)); this.scanAllExtensionPromise.set(profileLocation, scanAllExtensionsPromise); } @@ -592,9 +568,9 @@ export class ExtensionsScanner extends Disposable { } } - async scanAllUserExtensions(excludeOutdated: boolean): Promise { + async scanAllUserExtensions(): Promise { try { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + const scannedExtensions = await this.extensionsScannerService.scanAllUserExtensions(); return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } catch (error) { throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); @@ -613,7 +589,7 @@ export class ExtensionsScanner extends Disposable { return null; } - async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata, removeIfExists: boolean, token: CancellationToken): Promise { + async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, removeIfExists: boolean, token: CancellationToken): Promise { const folderName = extensionKey.toString(); const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`)); const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName)); @@ -648,6 +624,7 @@ export class ExtensionsScanner extends Disposable { throw fromExtractError(e); } + const metadata: Metadata = { installedTimestamp: Date.now() }; try { metadata.size = await computeSize(tempLocation, this.fileService); } catch (error) { @@ -691,13 +668,9 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extensionLocation, ExtensionType.User); } - async scanMetadata(local: ILocalExtension, profileLocation?: URI): Promise { - if (profileLocation) { - const extension = await this.getScannedExtension(local, profileLocation); - return extension?.metadata; - } else { - return this.extensionsScannerService.scanMetadata(local.location); - } + async scanMetadata(local: ILocalExtension, profileLocation: URI): Promise { + const extension = await this.getScannedExtension(local, profileLocation); + return extension?.metadata; } private async getScannedExtension(local: ILocalExtension, profileLocation: URI): Promise { @@ -763,7 +736,7 @@ export class ExtensionsScanner extends Disposable { await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation); } else { const targetExtension = await this.scanLocalExtension(target.location, extension.type, toProfileLocation); - await this.extensionsProfileScannerService.removeExtensionFromProfile(targetExtension, toProfileLocation); + await this.extensionsProfileScannerService.removeExtensionsFromProfile([targetExtension.identifier], toProfileLocation); await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, { ...target.metadata, ...metadata }]], toProfileLocation); } } else { @@ -890,11 +863,11 @@ export class ExtensionsScanner extends Disposable { }; } - private async initializeMetadata(): Promise { - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeInvalid: true }); + private async initializeExtensionSize(): Promise { + const extensions = await this.extensionsScannerService.scanAllUserExtensions(); await Promise.all(extensions.map(async extension => { // set size if not set before - if (!extension.metadata?.size && extension.metadata?.source !== 'resource') { + if (isDefined(extension.metadata?.installedTimestamp) && isUndefined(extension.metadata?.size)) { const size = await computeSize(extension.location, this.fileService); await this.extensionsScannerService.updateMetadata(extension.location, { size }); } @@ -916,7 +889,7 @@ export class ExtensionsScanner extends Disposable { this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions + const extensions = await this.scanAllUserExtensions(); const installed: Set = new Set(); for (const e of extensions) { if (!removed[ExtensionKey.create(e).toString()]) { @@ -930,14 +903,14 @@ export class ExtensionsScanner extends Disposable { await Promises.settled(byExtension.map(async e => { const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(await this.toLocalExtension(latest)); + await this.beforeRemovingExtension(latest); } })); } catch (error) { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); + const toRemove = extensions.filter(e => e.installedTimestamp /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); } @@ -1115,7 +1088,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } return undefined; @@ -1155,7 +1128,7 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem } protected doRun(token: CancellationToken): Promise { - return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + return this.extensionsProfileScannerService.removeExtensionsFromProfile([this.extension.identifier], this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 0329b90263b..a21b64a95a5 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -356,7 +356,7 @@ suite('ExtensionsProfileScannerService', () => { assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); }); - test('remove extension trigger events', async () => { + test('remove extensions trigger events', async () => { const testObject = disposables.add(instantiationService.createInstance(TestObject, extensionsLocation)); const target1 = sinon.stub(); const target2 = sinon.stub(); @@ -364,26 +364,33 @@ suite('ExtensionsProfileScannerService', () => { disposables.add(testObject.onDidRemoveExtensions(target2)); const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); - const extension = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); - await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest); - await testObject.removeExtensionFromProfile(extension, extensionsManifest); + const extension1 = aExtension('pub.a', joinPath(ROOT, 'foo', 'pub.a-1.0.0')); + const extension2 = aExtension('pub.b', joinPath(ROOT, 'foo', 'pub.b-1.0.0')); + await testObject.addExtensionsToProfile([[extension1, undefined], [extension2, undefined]], extensionsManifest); + await testObject.removeExtensionsFromProfile([extension1.identifier, extension2.identifier], extensionsManifest); const actual = await testObject.scanProfileExtensions(extensionsManifest); assert.deepStrictEqual(actual.length, 0); assert.ok(target1.calledOnce); assert.deepStrictEqual(((target1.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); - assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 1); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension.identifier); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension.manifest.version); - assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions.length, 2); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].identifier, extension2.identifier); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].version, extension2.manifest.version); + assert.deepStrictEqual(((target1.args[0][0])).extensions[1].location.toString(), extension2.location.toString()); assert.ok(target2.calledOnce); assert.deepStrictEqual(((target2.args[0][0])).profileLocation.toString(), extensionsManifest.toString()); - assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 1); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension.identifier); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension.manifest.version); - assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension.location.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions.length, 2); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].identifier, extension1.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].version, extension1.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[0].location.toString(), extension1.location.toString()); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].identifier, extension2.identifier); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].version, extension2.manifest.version); + assert.deepStrictEqual(((target2.args[0][0])).extensions[1].location.toString(), extension2.location.toString()); }); test('add extension with same id but different version', async () => { diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 551ba576d44..ea4108b2c17 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -105,12 +105,12 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].manifest, manifest); }); - test('scan user extension', async () => { + test('scan user extensions', async () => { const manifest: Partial = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } }); const extensionLocation = await aUserExtension(manifest); const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name', uuid: 'uuid' }); @@ -175,24 +175,24 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); }); - test('scan user extension with different versions', async () => { + test('scan all user extensions with different versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); assert.deepStrictEqual(actual[0].manifest.version, '1.0.2'); }); - test('scan user extension include all versions', async () => { + test('scan all user extensions include all versions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 2); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -201,35 +201,35 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[1].manifest.version, '1.0.2'); }); - test('scan user extension with different versions and higher version is not compatible', async () => { + test('scan all user extensions with different versions and higher version is not compatible', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); }); - test('scan exclude invalid extensions', async () => { + test('scan all user extensions exclude invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: false }); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); - test('scan include invalid extensions', async () => { + test('scan all user extensions include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({ includeInvalid: true }); + const actual = await testObject.scanAllUserExtensions({ includeAllVersions: false, includeInvalid: true }); assert.deepStrictEqual(actual.length, 2); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -257,12 +257,12 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].manifest.version, '1.0.0'); }); - test('scan extension with default nls replacements', async () => { + test('scan all user extensions with default nls replacements', async () => { const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); - const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + const testObject = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); - const actual = await testObject.scanUserExtensions({}); + const actual = await testObject.scanAllUserExtensions(); assert.deepStrictEqual(actual.length, 1); assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); @@ -277,11 +277,11 @@ suite('NativeExtensionsScanerService Test', () => { const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name': nlsLocation.fsPath }; - const actual = await testObject.scanUserExtensions({ language: 'en' }); + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, { language: 'en' }); - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN'); + assert.ok(actual !== null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.manifest.displayName, 'Hello World EN'); }); test('scan extension falls back to default nls replacements', async () => { @@ -292,11 +292,11 @@ suite('NativeExtensionsScanerService Test', () => { const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); translations = { 'pub.name2': nlsLocation.fsPath }; - const actual = await testObject.scanUserExtensions({ language: 'en' }); + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, { language: 'en' }); - assert.deepStrictEqual(actual.length, 1); - assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); - assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + assert.ok(actual !== null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.manifest.displayName, 'Hello World'); }); async function aUserExtension(manifest: Partial): Promise { diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index ee94de1090d..3855e37ce94 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -139,7 +139,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS } private async _scanBuiltinExtensions(language: string): Promise { - const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); + const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language }); return scannedExtensions.map(e => toExtensionDescription(e, false)); } diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 04d4ff9703d..9e6d46f7824 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -53,7 +53,7 @@ export class CachedExtensionScanner { try { const language = platform.language; const result = await Promise.allSettled([ - this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), + this._extensionsScannerService.scanSystemExtensions({ language, checkControlFile: true }), this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true }), this._environmentService.remoteAuthority ? [] : this._extensionManagementService.getInstalledWorkspaceExtensions(false) ]); @@ -86,7 +86,7 @@ export class CachedExtensionScanner { } try { - scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); + scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment([...scannedSystemExtensions, ...scannedUserExtensions], { language }); } catch (error) { this._logService.error(error); } From 85925efe02b1be4671c5f37af8386a75a8b35ecc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 6 Jan 2025 17:20:28 +0100 Subject: [PATCH 16/27] Allow deleting files via the explorer when a folder is marked as readonly (fix #195701) (#237342) --- .../files/browser/fileActions.contribution.ts | 37 ++++++++--------- .../contrib/files/browser/fileActions.ts | 40 ++++++++++++++----- .../files/browser/views/explorerView.ts | 4 +- .../workbench/contrib/files/common/files.ts | 2 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index be7dddf26ff..6cd04336cfb 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -14,7 +14,7 @@ import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMA import { CommandsRegistry, ICommandHandler } from '../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; +import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceWritableContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerResourceAvailableEditorIdsContext, FoldersViewVisibleContext } from '../common/files.js'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from '../../../browser/actions/workspaceCommands.js'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, REOPEN_WITH_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js'; import { AutoSaveAfterShortDelayContext } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; @@ -52,7 +52,7 @@ const RENAME_ID = 'renameFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: RENAME_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyCode.F2, mac: { primary: KeyCode.Enter @@ -64,7 +64,7 @@ const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: MOVE_FILE_TO_TRASH_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace, @@ -77,7 +77,7 @@ const DELETE_FILE_ID = 'deleteFile'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: FilesExplorerFocusCondition, primary: KeyMod.Shift | KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace @@ -88,7 +88,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DELETE_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext, ExplorerResourceMoveableToTrash.toNegated()), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceMoveableToTrash.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace @@ -100,7 +100,7 @@ const CUT_FILE_ID = 'filesExplorer.cut'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CUT_FILE_ID, weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyX, handler: cutFileHandler, }); @@ -121,7 +121,7 @@ CommandsRegistry.registerCommand(PASTE_FILE_ID, pasteFileHandler); KeybindingsRegistry.registerKeybindingRule({ id: `^${PASTE_FILE_ID}`, // the `^` enables pasting files into the explorer by preventing default bubble up weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext), + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceWritableContext), primary: KeyMod.CtrlCmd | KeyCode.KeyV, }); @@ -479,7 +479,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FILE_COMMAND_ID, title: NEW_FILE_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -490,7 +490,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: NEW_FOLDER_COMMAND_ID, title: NEW_FOLDER_LABEL, - precondition: ExplorerResourceNotReadonlyContext + precondition: ExplorerResourceWritableContext }, when: ExplorerFolderContext }); @@ -540,7 +540,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { id: CUT_FILE_ID, title: nls.localize('cut', "Cut"), }, - when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext) + when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceWritableContext) }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { @@ -559,7 +559,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: PASTE_FILE_ID, title: PASTE_FILE_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext, FileCopiedContext) + precondition: ContextKeyExpr.and(ExplorerResourceWritableContext, FileCopiedContext) }, when: ExplorerFolderContext }); @@ -593,8 +593,8 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ IsWebContext, // only on folders ExplorerFolderContext, - // only on editable folders - ExplorerResourceNotReadonlyContext + // only on writable folders + ExplorerResourceWritableContext ) })); @@ -638,7 +638,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: { id: RENAME_ID, title: TRIGGER_RENAME_LABEL, - precondition: ExplorerResourceNotReadonlyContext, + precondition: ExplorerResourceWritableContext, }, when: ExplorerRootContext.toNegated() }); @@ -648,13 +648,11 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: MOVE_FILE_TO_TRASH_ID, - title: MOVE_FILE_TO_TRASH_LABEL, - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: MOVE_FILE_TO_TRASH_LABEL }, alt: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext), + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash) }); @@ -664,8 +662,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { order: 20, command: { id: DELETE_FILE_ID, - title: nls.localize('deleteFile', "Delete Permanently"), - precondition: ExplorerResourceNotReadonlyContext, + title: nls.localize('deleteFile', "Delete Permanently") }, when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ExplorerResourceMoveableToTrash.toNegated()) }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 635765f09b7..50f786ea2f7 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -93,7 +93,7 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { +async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, filesConfigurationService: IFilesConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false, ignoreIfNotExists = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -109,7 +109,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer dirtyWorkingCopies.add(dirtyWorkingCopy); } } - let confirmed = true; + if (dirtyWorkingCopies.size) { let message: string; if (distinctElements.length > 1) { @@ -132,18 +132,40 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer }); if (!response.confirmed) { - confirmed = false; + return; } else { skipConfirm = true; } } - // Check if file is dirty in editor and save it to avoid data loss - if (!confirmed) { - return; + // Handle readonly + if (!skipConfirm) { + const readonlyResources = distinctElements.filter(e => filesConfigurationService.isReadonly(e.resource)); + if (readonlyResources.length) { + let message: string; + if (readonlyResources.length > 1) { + message = nls.localize('readonlyMessageFilesDelete', "You are deleting files that are configured to be read-only. Do you want to continue?"); + } else if (readonlyResources[0].isDirectory) { + message = nls.localize('readonlyMessageFolderOneDelete', "You are deleting a folder {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } else { + message = nls.localize('readonlyMessageFolderDelete', "You are deleting a file {0} that is configured to be read-only. Do you want to continue?", distinctElements[0].name); + } + + const response = await dialogService.confirm({ + type: 'warning', + message, + detail: nls.localize('continueDetail', "The read-only protection will be overridden if you continue."), + primaryButton: nls.localize('continueButtonLabel', "Continue") + }); + + if (!response.confirmed) { + return; + } + } } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command.") : nls.localize('restore', "You can restore this file using the Undo command."); @@ -234,7 +256,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer skipConfirm = true; ignoreIfNotExists = true; - return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); + return deleteFiles(explorerService, workingCopyFileService, dialogService, configurationService, filesConfigurationService, elements, useTrash, skipConfirm, ignoreIfNotExists); } } } @@ -1020,7 +1042,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, true); } }; @@ -1029,7 +1051,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFilesConfigurationService), stats, false); } }; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1c718837501..f91029d58d5 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from '../../../../../base/common/uri.js'; import * as perf from '../../../../../base/common/performance.js'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js'; import { memoize } from '../../../../../base/common/decorators.js'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceNotReadonlyContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, ExplorerResourceWritableContext, ViewHasSomeCollapsibleRootItemContext, FoldersViewVisibleContext, ExplorerResourceParentReadOnlyContext, ExplorerFindProviderActive } from '../../common/files.js'; import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from '../fileActions.js'; import * as DOM from '../../../../../base/browser/dom.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; @@ -988,7 +988,7 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, const CanCreateContext = ContextKeyExpr.or( // Folder: can create unless readonly - ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceNotReadonlyContext), + ContextKeyExpr.and(ExplorerFolderContext, ExplorerResourceWritableContext), // File: can create unless parent is readonly ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ExplorerResourceParentReadOnlyContext.toNegated()) ); diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 56715560f15..0eb091699a5 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -40,7 +40,7 @@ export const ExplorerViewletVisibleContext = new RawContextKey('explore export const FoldersViewVisibleContext = new RawContextKey('foldersViewVisible', true, { type: 'boolean', description: localize('foldersViewVisible', "True when the FOLDERS view (the file tree within the explorer view container) is visible.") }); export const ExplorerFolderContext = new RawContextKey('explorerResourceIsFolder', false, { type: 'boolean', description: localize('explorerResourceIsFolder', "True when the focused item in the EXPLORER is a folder.") }); export const ExplorerResourceReadonlyContext = new RawContextKey('explorerResourceReadonly', false, { type: 'boolean', description: localize('explorerResourceReadonly', "True when the focused item in the EXPLORER is read-only.") }); -export const ExplorerResourceNotReadonlyContext = ExplorerResourceReadonlyContext.toNegated(); +export const ExplorerResourceWritableContext = ExplorerResourceReadonlyContext.toNegated(); export const ExplorerResourceParentReadOnlyContext = new RawContextKey('explorerResourceParentReadonly', false, { type: 'boolean', description: localize('explorerResourceParentReadonly', "True when the focused item in the EXPLORER's parent is read-only.") }); /** From 3b92e711627e7da2a45f444afddb03f0359bd339 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 6 Jan 2025 17:32:21 +0100 Subject: [PATCH 17/27] /edits - accept hunk (#237341) * implement accept hunk * use reject over undo or discard --- .../chatEditingModifiedFileEntry.ts | 49 +++++++++++++-- .../contrib/chat/browser/chatEditorActions.ts | 28 +++++++++ .../chat/browser/chatEditorController.ts | 59 +++++++++++-------- .../contrib/chat/browser/chatEditorOverlay.ts | 4 ++ .../contrib/chat/common/chatEditingService.ts | 3 + 5 files changed, 114 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index aa105b2e767..37a6b9a2b68 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -14,6 +14,7 @@ import { EditOperation, ISingleEditOperation } from '../../../../../editor/commo 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 { IModelDeltaDecoration, ITextModel, OverviewRulerLane } from '../../../../../editor/common/model.js'; @@ -343,6 +344,41 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie }); } + async acceptHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + // diffInfo should have model version ids and check them (instead of the caller doing that) + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); + edits.push(EditOperation.replace(edit.originalRange, newText)); + } + this.docSnapshot.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + } + return true; + } + + async rejectHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.docSnapshot.getValueInRange(edit.originalRange); + edits.push(EditOperation.replace(edit.modifiedRange, newText)); + } + this.doc.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + } + return true; + } + private _applyEdits(edits: ISingleEditOperation[]) { // make the actual edit this._isEditFromUs = true; @@ -358,13 +394,14 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } } - private _updateDiffInfoSeq() { + private async _updateDiffInfoSeq() { const myDiffOperationId = ++this._diffOperationIds; - Promise.resolve(this._diffOperation).then(() => { - if (this._diffOperationIds === myDiffOperationId) { - this._diffOperation = this._updateDiffInfo(); - } - }); + await Promise.resolve(this._diffOperation); + if (this._diffOperationIds === myDiffOperationId) { + const thisDiffOperation = this._updateDiffInfo(); + this._diffOperation = thisDiffOperation; + await thisDiffOperation; + } } private async _updateDiffInfo(): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index b7f3ae1d0b4..a8060733fff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -217,6 +217,33 @@ class UndoHunkAction extends EditorAction2 { } } +class AcceptHunkAction extends EditorAction2 { + constructor() { + super({ + id: 'chatEditor.action.acceptHunk', + title: localize2('acceptHunk', 'Accept this Change'), + shortTitle: localize2('acceptHunk2', 'Accept'), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + icon: Codicon.check, + f1: true, + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter + }, + menu: { + id: MenuId.ChatEditingEditorHunk, + order: 0 + } + }); + } + + override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]) { + ChatEditorController.get(editor)?.acceptNearestChange(args[0]); + } +} + class OpenDiffFromHunkAction extends EditorAction2 { constructor() { super({ @@ -243,5 +270,6 @@ export function registerChatEditorActions() { registerAction2(AcceptAction); registerAction2(RejectAction); registerAction2(UndoHunkAction); + registerAction2(AcceptHunkAction); registerAction2(OpenDiffFromHunkAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index e2a186b5886..bf0c199267f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -12,7 +12,6 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPosi import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; import { EditorOption, IEditorStickyScrollOptions } from '../../../../editor/common/config/editorOptions.js'; -import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; @@ -31,6 +30,7 @@ import { Selection } from '../../../../editor/common/core/selection.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; +import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); @@ -328,13 +328,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut } // Add content widget for each diff change - const undoEdits: ISingleEditOperation[] = []; - for (const c of diffEntry.innerChanges ?? []) { - const oldText = originalModel.getValueInRange(c.originalRange); - undoEdits.push(EditOperation.replace(c.modifiedRange, oldText)); - } - - const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, undoEdits, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); + const widget = this._instantiationService.createInstance(DiffHunkWidget, entry, diffEntry, this._editor.getModel()!.getVersionId(), this._editor, isCreatedContent ? 0 : result.heightInLines); widget.layout(diffEntry.modified.startLineNumber); this._diffHunkWidgets.push(widget); @@ -492,28 +486,41 @@ export class ChatEditorController extends Disposable implements IEditorContribut return true; } - undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + private _findClosestWidget(): DiffHunkWidget | undefined { if (!this._editor.hasModel()) { - return; + return undefined; } const lineRelativeTop = this._editor.getTopForLineNumber(this._editor.getPosition().lineNumber) - this._editor.getScrollTop(); + let closestWidget: DiffHunkWidget | undefined; let closestDistance = Number.MAX_VALUE; - if (!(closestWidget instanceof DiffHunkWidget)) { - for (const widget of this._diffHunkWidgets) { - const widgetTop = (widget.getPosition()?.preference)?.top; - if (widgetTop !== undefined) { - const distance = Math.abs(widgetTop - lineRelativeTop); - if (distance < closestDistance) { - closestDistance = distance; - closestWidget = widget; - } + for (const widget of this._diffHunkWidgets) { + const widgetTop = (widget.getPosition()?.preference)?.top; + if (widgetTop !== undefined) { + const distance = Math.abs(widgetTop - lineRelativeTop); + if (distance < closestDistance) { + closestDistance = distance; + closestWidget = widget; } } } + return closestWidget; + } + + undoNearestChange(closestWidget: DiffHunkWidget | undefined): void { + closestWidget = closestWidget ?? this._findClosestWidget(); if (closestWidget instanceof DiffHunkWidget) { - closestWidget.undo(); + closestWidget.reject(); + this.revealNext(); + } + } + + acceptNearestChange(closestWidget: DiffHunkWidget | undefined): void { + closestWidget = closestWidget ?? this._findClosestWidget(); + if (closestWidget instanceof DiffHunkWidget) { + closestWidget.accept(); + this.revealNext(); } } @@ -570,7 +577,7 @@ class DiffHunkWidget implements IOverlayWidget { constructor( readonly entry: IModifiedFileEntry, - private readonly _undoEdits: ISingleEditOperation[], + private readonly _change: DetailedLineRangeMapping, private readonly _versionId: number, private readonly _editor: ICodeEditor, private readonly _lineDelta: number, @@ -641,9 +648,15 @@ class DiffHunkWidget implements IOverlayWidget { // --- - undo() { + reject(): void { if (this._versionId === this._editor.getModel()?.getVersionId()) { - this._editor.executeEdits('chatEdits.undo', this._undoEdits); + this.entry.rejectHunk(this._change); + } + } + + accept(): void { + if (this._versionId === this._editor.getModel()?.getVersionId()) { + this.entry.acceptHunk(this._change); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index bb94d4e0fc9..4b0dd3c8111 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -310,6 +310,10 @@ export class ChatEditorOverlayController implements IEditorContribution { } const entry = entries[idx]; + if (entry.state.read(r) === WorkingSetEntryState.Accepted || entry.state.read(r) === WorkingSetEntryState.Rejected) { + widget.hide(); + return; + } widget.show(session, entry, entries[(idx + 1) % entries.length]); })); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index ed970c30625..57e0924ce48 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -10,6 +10,7 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { IObservable, IReader, ITransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; @@ -120,6 +121,8 @@ export interface IModifiedFileEntry { readonly rewriteRatio: IObservable; readonly maxLineNumber: IObservable; readonly diffInfo: IObservable; + acceptHunk(change: DetailedLineRangeMapping): Promise; + rejectHunk(change: DetailedLineRangeMapping): Promise; readonly lastModifyingRequestId: string; accept(transaction: ITransaction | undefined): Promise; reject(transaction: ITransaction | undefined): Promise; From abe43ed1d5257afa420579370cb0b45818e1d89d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:31:52 +0100 Subject: [PATCH 18/27] Git - add author email to the blame/graph hover (#237360) --- extensions/git/src/blame.ts | 7 ++++++- extensions/git/src/historyProvider.ts | 1 + src/vs/workbench/api/common/extHost.protocol.ts | 1 + .../workbench/contrib/scm/browser/scmHistoryViewPane.ts | 8 ++++++-- src/vs/workbench/contrib/scm/common/history.ts | 1 + src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts | 1 + 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 0dbdca92b32..a39781cdc41 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -227,7 +227,12 @@ export class GitBlameController { markdownString.supportThemeIcons = true; if (blameInformationOrCommit.authorName) { - markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); + if (blameInformationOrCommit.authorEmail) { + const emailTitle = l10n.t('Email'); + markdownString.appendMarkdown(`$(account) [**${blameInformationOrCommit.authorName}**](mailto:${blameInformationOrCommit.authorEmail} "${emailTitle} ${blameInformationOrCommit.authorName}")`); + } else { + markdownString.appendMarkdown(`$(account) **${blameInformationOrCommit.authorName}**`); + } if (blameInformationOrCommit.authorDate) { const dateString = new Date(blameInformationOrCommit.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 250cf80560d..b18923d56b7 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -260,6 +260,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec parentIds: commit.parents, message: emojify(commit.message), author: commit.authorName, + authorEmail: commit.authorEmail, icon: new ThemeIcon('git-commit'), displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash), timestamp: commit.authorDate?.getTime(), diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 36e385c3e6e..7bccc00f5b9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1606,6 +1606,7 @@ export interface SCMHistoryItemDto { readonly message: string; readonly displayId?: string; readonly author?: string; + readonly authorEmail?: string; readonly timestamp?: number; readonly statistics?: { readonly files: number; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index af17ed451bd..1c3f71ac1ca 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -459,10 +459,14 @@ class HistoryItemRenderer implements ITreeRenderer Date: Mon, 6 Jan 2025 11:44:04 -0800 Subject: [PATCH 19/27] fix: don't try to move chat editing session to a chat editor (#237362) --- .../workbench/contrib/chat/browser/actions/chatMoveActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts index e927be89e77..e732c01efbb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatMoveActions.ts @@ -18,6 +18,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { isChatViewTitleActionContext } from '../../common/chatActions.js'; +import { ChatAgentLocation } from '../../common/chatAgents.js'; enum MoveToNewLocation { Editor = 'Editor', @@ -99,7 +100,7 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew const widget = (_sessionId ? widgetService.getWidgetBySessionId(_sessionId) : undefined) ?? widgetService.lastFocusedWidget; - if (!widget || !('viewId' in widget.viewContext)) { + if (!widget || widget.location !== ChatAgentLocation.Panel) { await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); return; } From e22e3e729360d29e15b6bf3e0301b9a9eff4770e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:47:43 +0100 Subject: [PATCH 20/27] Git - improve timeline hover (#237365) --- extensions/git/src/blame.ts | 6 +-- extensions/git/src/commands.ts | 8 ++-- extensions/git/src/git.ts | 5 ++ extensions/git/src/timelineProvider.ts | 47 +++++++++++++++---- .../contrib/timeline/browser/timelinePane.ts | 2 +- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index a39781cdc41..c4ca348eef9 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -265,9 +265,9 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); - markdownString.appendMarkdown(`[$(copy)](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); markdownString.appendMarkdown(`[$(gear)](command:workbench.action.openSettings?%5B%22git.blame%22%5D "${l10n.t('Open Settings')}")`); @@ -702,7 +702,7 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), - command: 'git.blameStatusBarItem.viewCommit', + command: 'git.viewCommit2', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f7476a23214..ab344ad5023 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4346,8 +4346,8 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } - @command('git.blameStatusBarItem.viewCommit', { repository: true }) - async viewStatusBarCommit(repository: Repository, historyItemId: string): Promise { + @command('git.viewCommit2', { repository: true }) + async viewCommit2(repository: Repository, historyItemId: string): Promise { if (!repository || !historyItemId) { return; } @@ -4365,8 +4365,8 @@ export class CommandCenter { await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); } - @command('git.blameStatusBarItem.copyContent') - async blameStatusBarCopyContent(content: string): Promise { + @command('git.copyContentToClipboard') + async copyContentToClipboard(content: string): Promise { if (typeof content !== 'string') { return; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 5bdfd655dbc..0ca691fda64 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -62,6 +62,7 @@ export interface LogFileOptions { /** Optional. Specifies whether to start retrieving log entries in reverse order. */ readonly reverse?: boolean; readonly sortByAuthorDate?: boolean; + readonly shortStats?: boolean; } function parseVersion(raw: string): string { @@ -1290,6 +1291,10 @@ export class Repository { } } + if (options?.shortStats) { + args.push('--shortstat'); + } + if (options?.sortByAuthorDate) { args.push('--author-date-order'); } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 5788ecc53dd..4f5c53f9e50 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -10,6 +10,8 @@ import { debounce } from './decorators'; import { emojify, ensureEmojis } from './emoji'; import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; +import { getCommitShortHash } from './util'; +import { CommitShortStat } from './git'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -48,18 +50,46 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } - setItemDetails(author: string, email: string | undefined, date: string, message: string): void { + setItemDetails(uri: Uri, hash: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat): void { this.tooltip = new MarkdownString('', true); + this.tooltip.isTrusted = true; + this.tooltip.supportHtml = true; if (email) { const emailTitle = l10n.t('Email'); - this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")\n\n`); + this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")`); } else { - this.tooltip.appendMarkdown(`$(account) **${author}**\n\n`); + this.tooltip.appendMarkdown(`$(account) **${author}**`); } - this.tooltip.appendMarkdown(`$(history) ${date}\n\n`); - this.tooltip.appendMarkdown(message); + this.tooltip.appendMarkdown(`, $(history) ${date}\n\n`); + this.tooltip.appendMarkdown(`${message}\n\n`); + + if (shortStat) { + this.tooltip.appendMarkdown(`---\n\n`); + + if (shortStat.insertions) { + this.tooltip.appendMarkdown(`${shortStat.insertions === 1 ? + l10n.t('{0} insertion{1}', shortStat.insertions, '(+)') : + l10n.t('{0} insertions{1}', shortStat.insertions, '(+)')}`); + } + + if (shortStat.deletions) { + this.tooltip.appendMarkdown(`, ${shortStat.deletions === 1 ? + l10n.t('{0} deletion{1}', shortStat.deletions, '(-)') : + l10n.t('{0} deletions{1}', shortStat.deletions, '(-)')}`); + } + + this.tooltip.appendMarkdown(`\n\n`); + } + + if (hash) { + this.tooltip.appendMarkdown(`---\n\n`); + + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(' '); + this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); + } } private shortenRef(ref: string): string { @@ -153,6 +183,7 @@ export class GitTimelineProvider implements TimelineProvider { maxEntries: limit, hash: options.cursor, follow: true, + shortStats: true, // sortByAuthorDate: true }); @@ -184,7 +215,7 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - item.setItemDetails(c.authorName!, c.authorEmail, dateFormatter.format(date), message); + item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -209,7 +240,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(you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -231,7 +262,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(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(uri, undefined, you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index b97f9201953..7e18e20482c 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -1162,7 +1162,7 @@ class TimelineTreeRenderer implements ITreeRenderer Date: Mon, 6 Jan 2025 21:48:07 +0100 Subject: [PATCH 21/27] fix #236194 (#237366) --- .../common/extensionsScannerService.ts | 138 ++++++++++-------- .../node/extensionManagementService.ts | 10 +- 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index f852d912536..0800126329d 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -609,15 +609,16 @@ class ExtensionsScanner extends Disposable { if (!scannedProfileExtensions.length) { return []; } - const extensions = await Promise.all( - scannedProfileExtensions.map(async extensionInfo => { - if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); - return this.scanExtension(extensionScannerInput, extensionInfo.metadata); - } - return null; - })); - return coalesce(extensions); + const extensions: IRelaxedScannedExtension[] = []; + await Promise.all(scannedProfileExtensions.map(async extensionInfo => { + if (!filter(extensionInfo)) { + return; + } + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extension = await this.scanExtension(extensionScannerInput, extensionInfo); + extensions.push(extension); + })); + return extensions; } async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise { @@ -634,56 +635,76 @@ class ExtensionsScanner extends Disposable { } } - async scanExtension(input: ExtensionScannerInput, metadata?: Metadata): Promise { + async scanExtension(input: ExtensionScannerInput): Promise; + async scanExtension(input: ExtensionScannerInput, scannedProfileExtension: IScannedProfileExtension): Promise; + async scanExtension(input: ExtensionScannerInput, scannedProfileExtension?: IScannedProfileExtension): Promise { + const validations: [Severity, string][] = []; + let isValid = true; + let manifest: IScannedExtensionManifest; try { - let manifest = await this.scanExtensionManifest(input.location); - if (manifest) { - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!manifest.publisher) { - manifest.publisher = UNDEFINED_PUBLISHER; - } - metadata = metadata ?? manifest.__metadata; - if (metadata && !metadata?.size && manifest.__metadata?.size) { - metadata.size = manifest.__metadata?.size; - } - delete manifest.__metadata; - const id = getGalleryExtensionId(manifest.publisher, manifest.name); - const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; - const type = metadata?.isSystem ? ExtensionType.System : input.type; - const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; - manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); - let extension: IRelaxedScannedExtension = { - type, - identifier, - manifest, - location: input.location, - isBuiltin, - targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, - publisherDisplayName: metadata?.publisherDisplayName, - metadata, - isValid: true, - validations: [], - preRelease: !!metadata?.preRelease, - }; - if (input.validate) { - extension = this.validate(extension, input); - } - if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { - manifest.originalEnabledApiProposals = manifest.enabledApiProposals; - manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); - } - return extension; - } + manifest = await this.scanExtensionManifest(input.location); } catch (e) { - if (input.type !== ExtensionType.System) { - this.logService.error(e); + if (scannedProfileExtension) { + validations.push([Severity.Error, getErrorMessage(e)]); + isValid = false; + const [publisher, name] = scannedProfileExtension.identifier.id.split('.'); + manifest = { + name, + publisher, + version: scannedProfileExtension.version, + engines: { vscode: '' } + }; + } else { + if (input.type !== ExtensionType.System) { + this.logService.error(e); + } + return null; } } - return null; + + // allow publisher to be undefined to make the initial extension authoring experience smoother + if (!manifest.publisher) { + manifest.publisher = UNDEFINED_PUBLISHER; + } + const metadata = scannedProfileExtension?.metadata ?? manifest.__metadata; + if (metadata && !metadata?.size && manifest.__metadata?.size) { + metadata.size = manifest.__metadata?.size; + } + delete manifest.__metadata; + const id = getGalleryExtensionId(manifest.publisher, manifest.name); + const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; + const type = metadata?.isSystem ? ExtensionType.System : input.type; + const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; + try { + manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); + } catch (error) { + this.logService.warn('Failed to translate manifest', getErrorMessage(error)); + } + let extension: IRelaxedScannedExtension = { + type, + identifier, + manifest, + location: input.location, + isBuiltin, + targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, + publisherDisplayName: metadata?.publisherDisplayName, + metadata, + isValid, + validations, + preRelease: !!metadata?.preRelease, + }; + if (input.validate) { + extension = this.validate(extension, input); + } + if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) { + manifest.originalEnabledApiProposals = manifest.enabledApiProposals; + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return extension; } validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { - let isValid = true; + let isValid = extension.isValid; const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); for (const [severity, message] of validations) { @@ -693,11 +714,11 @@ class ExtensionsScanner extends Disposable { } } extension.isValid = isValid; - extension.validations = validations; + extension.validations = [...extension.validations, ...validations]; return extension; } - private async scanExtensionManifest(extensionLocation: URI): Promise { + private async scanExtensionManifest(extensionLocation: URI): Promise { const manifestLocation = joinPath(extensionLocation, 'package.json'); let content; try { @@ -706,7 +727,7 @@ class ExtensionsScanner extends Disposable { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message))); } - return null; + throw error; } let manifest: IScannedExtensionManifest; try { @@ -718,11 +739,12 @@ class ExtensionsScanner extends Disposable { for (const e of errors) { this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error)))); } - return null; + throw err; } if (getNodeType(manifest) !== 'object') { - this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path))); - return null; + const errorMessage = this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path)); + this.logService.error(errorMessage); + throw new Error(errorMessage); } return manifest; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e9b38bc69f2..e954abf073b 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -45,7 +45,7 @@ import { ExtensionsManifestCache } from './extensionsManifestCache.js'; import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from './extensionsWatcher.js'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { isEngineValid } from '../../extensions/common/extensionValidator.js'; -import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; +import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, IFileStat, toFileOperationResult } from '../../files/common/files.js'; import { IInstantiationService, refineServiceDecorator } from '../../instantiation/common/instantiation.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; @@ -829,10 +829,14 @@ export class ExtensionsScanner extends Disposable { } private async toLocalExtension(extension: IScannedExtension): Promise { - const stat = await this.fileService.resolve(extension.location); + let stat: IFileStat | undefined; + try { + stat = await this.fileService.resolve(extension.location); + } catch (error) {/* ignore */ } + let readmeUrl: URI | undefined; let changelogUrl: URI | undefined; - if (stat.children) { + if (stat?.children) { readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; } From fc4e78cbfe5c7e5e365ebe9fd43b7cf157d229c3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:17:09 +0100 Subject: [PATCH 22/27] Git - remove commands that are not used (#237368) Git - remove commands that are not used --- extensions/git/package.json | 11 ----- extensions/git/package.nls.json | 1 - extensions/git/src/blame.ts | 4 +- extensions/git/src/commands.ts | 61 +------------------------- extensions/git/src/timelineProvider.ts | 2 +- 5 files changed, 5 insertions(+), 74 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 064dfe3f1cf..66e8dd4981d 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -912,13 +912,6 @@ "category": "Git", "enablement": "!operationInProgress" }, - { - "command": "git.viewAllChanges", - "title": "%command.viewAllChanges%", - "icon": "$(diff-multiple)", - "category": "Git", - "enablement": "!operationInProgress" - }, { "command": "git.copyCommitId", "title": "%command.timelineCopyCommitId%", @@ -1444,10 +1437,6 @@ "command": "git.viewCommit", "when": "false" }, - { - "command": "git.viewAllChanges", - "when": "false" - }, { "command": "git.stageFile", "when": "false" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index afbb44ba48f..e7020f41890 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -125,7 +125,6 @@ "command.viewChanges": "View Changes", "command.viewStagedChanges": "View Staged Changes", "command.viewUntrackedChanges": "View Untracked Changes", - "command.viewAllChanges": "View All Changes", "command.viewCommit": "View Commit", "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index c4ca348eef9..c8f15ffdf94 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -265,7 +265,7 @@ export class GitBlameController { markdownString.appendMarkdown(`\n\n---\n\n`); } - markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(documentUri, blameInformationOrCommit.hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformationOrCommit.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(blameInformationOrCommit.hash))} "${l10n.t('Copy Commit Hash')}")`); markdownString.appendMarkdown('  |  '); @@ -702,7 +702,7 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = this._controller.getBlameInformationHover(window.activeTextEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), - command: 'git.viewCommit2', + command: 'git.viewCommit', arguments: [window.activeTextEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index ab344ad5023..b8fa9009e1f 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4271,63 +4271,6 @@ export class CommandCenter { }); } - @command('git.viewCommit', { repository: true }) - async viewCommit(repository: Repository, historyItem1: SourceControlHistoryItem, historyItem2?: SourceControlHistoryItem): Promise { - if (!repository || !historyItem1) { - return; - } - - if (historyItem2) { - const mergeBase = await repository.getMergeBase(historyItem1.id, historyItem2.id); - if (!mergeBase || (mergeBase !== historyItem1.id && mergeBase !== historyItem2.id)) { - return; - } - } - - let title: string | undefined; - let historyItemParentId: string | undefined; - const rootUri = Uri.file(repository.root); - - // If historyItem2 is not provided, we are viewing a single commit. If historyItem2 is - // provided, we are viewing a range and we have to include both start and end commits. - // TODO@lszomoru - handle the case when historyItem2 is the first commit in the repository - if (!historyItem2) { - const commit = await repository.getCommit(historyItem1.id); - title = `${getCommitShortHash(rootUri, historyItem1.id)} - ${truncate(commit.message)}`; - historyItemParentId = historyItem1.parentIds.length > 0 ? historyItem1.parentIds[0] : `${historyItem1.id}^`; - } else { - title = l10n.t('All Changes ({0} ↔ {1})', getCommitShortHash(rootUri, historyItem2.id), getCommitShortHash(rootUri, historyItem1.id)); - historyItemParentId = historyItem2.parentIds.length > 0 ? historyItem2.parentIds[0] : `${historyItem2.id}^`; - } - - const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItem1.id}` }); - - await this._viewChanges(repository, historyItem1.id, historyItemParentId, multiDiffSourceUri, title); - } - - @command('git.viewAllChanges', { repository: true }) - async viewAllChanges(repository: Repository, historyItem: SourceControlHistoryItem): Promise { - if (!repository || !historyItem) { - return; - } - - const rootUri = Uri.file(repository.root); - const modifiedShortRef = getCommitShortHash(rootUri, historyItem.id); - const originalShortRef = historyItem.parentIds.length > 0 ? getCommitShortHash(rootUri, historyItem.parentIds[0]) : `${modifiedShortRef}^`; - const title = l10n.t('All Changes ({0} ↔ {1})', originalShortRef, modifiedShortRef); - - const multiDiffSourceUri = toGitUri(Uri.file(repository.root), historyItem.id, { scheme: 'git-changes' }); - - await this._viewChanges(repository, modifiedShortRef, originalShortRef, multiDiffSourceUri, title); - } - - async _viewChanges(repository: Repository, historyItemId: string, historyItemParentId: string, multiDiffSourceUri: Uri, title: string): Promise { - const changes = await repository.diffBetween(historyItemParentId, historyItemId); - const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); - - await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); - } - @command('git.copyCommitId', { repository: true }) async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise { if (!repository || !historyItem) { @@ -4346,8 +4289,8 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } - @command('git.viewCommit2', { repository: true }) - async viewCommit2(repository: Repository, historyItemId: string): Promise { + @command('git.viewCommit', { repository: true }) + async viewCommit(repository: Repository, historyItemId: string): Promise { if (!repository || !historyItemId) { return; } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 4f5c53f9e50..2b3da08f82e 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -86,7 +86,7 @@ export class GitTimelineItem extends TimelineItem { if (hash) { this.tooltip.appendMarkdown(`---\n\n`); - this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit2?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); + this.tooltip.appendMarkdown(`[\`$(git-commit) ${getCommitShortHash(uri, hash)} \`](command:git.viewCommit?${encodeURIComponent(JSON.stringify([uri, hash]))} "${l10n.t('View Commit')}")`); this.tooltip.appendMarkdown(' '); this.tooltip.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); } From 70866d528727b9131fc07b0f14869f256059949b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 6 Jan 2025 21:22:42 -0700 Subject: [PATCH 23/27] Enable "tools agent" (#237369) * Add an edit tool (doesn't work) * More * Properly wait on text edits to be done applying * Better editFile tool * Fixes * Be more insistent with editFile instructions * Add "agent mode" UI * Fix error thrown when calling tools sometimes * Persist chat agent mode state * Hide editing tools from other extensions for now * Fix test build issues * Allow disabling tools agent mode * Remove comment * Fix codeblock index properly * Cleanup * Cleanup * Remove ccreq check * Rename for clarity --- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../api/common/extHostLanguageModelTools.ts | 27 ++- .../browser/actions/chatExecuteActions.ts | 49 +++++- .../contrib/chat/browser/chat.contribution.ts | 2 + .../chatMarkdownContentPart.ts | 25 +-- .../browser/chatParticipant.contribution.ts | 3 +- .../contrib/chat/browser/codeBlockPart.ts | 1 + .../browser/contrib/chatInputCompletions.ts | 17 +- .../chat/browser/languageModelToolsService.ts | 24 ++- .../contrib/chat/browser/tools/tools.ts | 157 ++++++++++++++++++ .../contrib/chat/common/chatAgents.ts | 48 +++++- .../contrib/chat/common/chatContextKeys.ts | 5 + .../common/chatParticipantContribTypes.ts | 1 + .../chatProgressTypes/chatToolInvocation.ts | 4 - .../contrib/chat/common/chatViewModel.ts | 18 +- .../chat/test/common/chatAgents.test.ts | 3 +- .../chat/test/common/voiceChatService.test.ts | 22 ++- 17 files changed, 351 insertions(+), 59 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/tools/tools.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 868906908ea..a2fc5b81c0f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1500,10 +1500,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageModelTools.registerTool(extension, name, tool); }, invokeTool(name: string, parameters: vscode.LanguageModelToolInvocationOptions, token?: vscode.CancellationToken) { - return extHostLanguageModelTools.invokeTool(name, parameters, token); + return extHostLanguageModelTools.invokeTool(extension, name, parameters, token); }, get tools() { - return extHostLanguageModelTools.tools; + return extHostLanguageModelTools.getTools(extension); }, fileIsIgnored(uri: vscode.Uri, token: vscode.CancellationToken) { return extHostLanguageModels.fileIsIgnored(extension, uri, token); diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index f078897e24a..b2f25fb152f 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -14,6 +14,7 @@ import { IExtensionDescription } from '../../../platform/extensions/common/exten import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; @@ -45,17 +46,22 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return await fn(input, token); } - async invokeTool(toolId: string, options: vscode.LanguageModelToolInvocationOptions, token?: CancellationToken): Promise { + async invokeTool(extension: IExtensionDescription, toolId: string, options: vscode.LanguageModelToolInvocationOptions, token?: CancellationToken): Promise { const callId = generateUuid(); if (options.tokenizationOptions) { this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens); } - if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) { - throw new Error(`Invalid tool invocation token`); - } - try { + if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) { + throw new Error(`Invalid tool invocation token`); + } + + const tool = this._allTools.get(toolId); + if (tool?.tags?.includes('vscode_editing') && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + throw new Error(`Invalid tool: ${toolId}`); + } + // Making the round trip here because not all tools were necessarily registered in this EH const result = await this._proxy.$invokeTool({ toolId, @@ -77,9 +83,16 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape } } - get tools(): vscode.LanguageModelToolInformation[] { + getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] { return Array.from(this._allTools.values()) - .map(tool => typeConvert.LanguageModelToolDescription.to(tool)); + .map(tool => typeConvert.LanguageModelToolDescription.to(tool)) + .filter(tool => { + if (tool.tags.includes('vscode_editing')) { + return isProposedApiEnabled(extension, 'chatParticipantPrivate'); + } + + return true; + }); } async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 1b8f11d4851..6557d40e8c7 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -6,11 +6,11 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { URI } from '../../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; @@ -75,6 +75,52 @@ export class ChatSubmitAction extends SubmitAction { } } +export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; +export class ToggleAgentModeAction extends Action2 { + static readonly ID = ToggleAgentModeActionId; + + constructor() { + super({ + id: ToggleAgentModeAction.ID, + title: localize2('interactive.toggleAgent.label', "Toggle Agent Mode"), + f1: true, + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ChatContextKeys.Editing.hasToolsAgent), + icon: Codicon.edit, + toggled: { + condition: ChatContextKeys.Editing.agentMode, + icon: Codicon.tools, + tooltip: localize('agentEnabled', "Agent Mode Enabled"), + }, + tooltip: localize('agentDisabled', "Agent Mode Disabled"), + keybinding: { + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), + primary: KeyMod.CtrlCmd | KeyCode.Period, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + id: MenuId.ChatExecute, + order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ChatContextKeys.Editing.hasToolsAgent), + group: 'navigation', + }, + ] + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const agentService = accessor.get(IChatAgentService); + agentService.toggleToolsAgentMode(); + } +} + export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; @@ -388,4 +434,5 @@ export function registerChatExecuteActions() { registerAction2(SendToNewChatAction); registerAction2(ChatSubmitSecondaryAgentAction); registerAction2(SendToChatEditingAction); + registerAction2(ToggleAgentModeAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 456c8817cd4..86b780b350c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -81,6 +81,7 @@ import { Extensions, IConfigurationMigrationRegistry } from '../../../common/con import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { ChatQuotasService, ChatQuotasStatusBarEntry, IChatQuotasService } from './chatQuotasService.js'; +import { BuiltinToolsContribution } from './tools/tools.js'; import { ChatSetupContribution } from './chatSetup.js'; // Register configuration @@ -319,6 +320,7 @@ registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandl registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatQuotasStatusBarEntry.ID, ChatQuotasStatusBarEntry, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribution, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index a281df1c40c..aad084c2bb2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -76,17 +76,22 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering const orderedDisposablesList: IDisposable[] = []; - let codeBlockIndex = codeBlockStartIndex; + + // Need to track the index of the codeblock within the response so it can have a unique ID, + // and within this part to find it within the codeblocks array + let globalCodeBlockIndexStart = codeBlockStartIndex; + let thisPartCodeBlockIndexStart = 0; const result = this._register(renderer.render(markdown.content, { fillInIncompleteTokens, codeBlockRendererSync: (languageId, text, raw) => { - const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.endsWith('```'); + const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || raw?.trim().endsWith('```'); if ((!text || (text.startsWith('') && !text.includes('\n'))) && !isCodeBlockComplete && rendererOptions.renderCodeBlockPills) { const hideEmptyCodeblock = $('div'); hideEmptyCodeblock.style.display = 'none'; return hideEmptyCodeblock; } - const index = codeBlockIndex++; + const globalIndex = globalCodeBlockIndexStart++; + const thisPartIndex = thisPartCodeBlockIndexStart++; let textModel: Promise; let range: Range | undefined; let vulns: readonly IMarkdownVulnerability[] | undefined; @@ -101,15 +106,15 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP } } else { const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); - const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, index, { text, languageId, isComplete: isCodeBlockComplete }); + const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, globalIndex); + const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, globalIndex, { text, languageId, isComplete: isCodeBlockComplete }); vulns = modelEntry.vulns; codemapperUri = fastUpdateModelEntry.codemapperUri; textModel = modelEntry.model; } const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }; + const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }; if (!rendererOptions.renderCodeBlockPills || element.isCompleteAddedRequest || !codemapperUri) { const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth); @@ -122,7 +127,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ownerMarkdownPartId = this.id; const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = index; + readonly codeBlockIndex = globalIndex; readonly element = element; readonly isStreaming = !rendererOptions.renderCodeBlockPills; codemapperUri = undefined; // will be set async @@ -149,7 +154,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP // TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => { // Update the existing object's codemapperUri - this.codeblocks[codeBlockInfo.codeBlockIndex].codemapperUri = e.codemapperUri; + this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri; this._onDidChangeHeight.fire(); }); } @@ -157,7 +162,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ownerMarkdownPartId = this.id; const info: IChatCodeBlockInfo = new class { readonly ownerMarkdownPartId = ownerMarkdownPartId; - readonly codeBlockIndex = index; + readonly codeBlockIndex = globalIndex; readonly element = element; readonly isStreaming = !isCodeBlockComplete; readonly codemapperUri = codemapperUri; @@ -205,7 +210,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP if (isResponseVM(data.element)) { this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId, isComplete }).then((e) => { // Update the existing object's codemapperUri - this.codeblocks[data.codeBlockIndex].codemapperUri = e.codemapperUri; + this.codeblocks[data.codeBlockPartIndex].codemapperUri = e.codemapperUri; this._onDidChangeHeight.fire(); }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 3d16ee993b7..6949c731803 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -199,7 +199,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { + if ((providerDescriptor.isDefault || providerDescriptor.isAgent) && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); continue; } @@ -245,6 +245,7 @@ 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], diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 6f34a50203b..29ed76b29c4 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -74,6 +74,7 @@ const $ = dom.$; export interface ICodeBlockData { readonly codeBlockIndex: number; + readonly codeBlockPartIndex: number; readonly element: unknown; readonly textModel: Promise; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index bf5dfa67cef..68e436eeeef 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; import { raceTimeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { isPatternInWord } from '../../../../../base/common/filters.js'; @@ -258,7 +259,11 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { + coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + return; + } + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const label = `${agentLabel} ${chatSubcommandLeader}${c.name}`; const item: CompletionItem = { @@ -284,7 +289,7 @@ class AgentCompletions extends Disposable { } return item; - }))) + })))) }; } })); @@ -313,7 +318,11 @@ class AgentCompletions extends Disposable { .filter(a => a.locations.includes(widget.location)); return { - suggestions: agents.flatMap(agent => agent.slashCommands.map((c, i) => { + suggestions: coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + return; + } + const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); const withSlash = `${chatSubcommandLeader}${c.name}`; const extraSortText = agent.id === 'github.copilot.terminalPanel' ? `z` : ``; @@ -338,7 +347,7 @@ class AgentCompletions extends Disposable { } return item; - })) + }))) }; } })); diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index d0bce841451..d53c0db6859 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -13,6 +13,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from import { localize } from '../../../../nls.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ChatModel } from '../common/chatModel.js'; @@ -46,6 +47,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo @IChatService private readonly _chatService: IChatService, @IDialogService private readonly _dialogService: IDialogService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, ) { super(); @@ -125,6 +127,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`); + // When invoking a tool, don't validate the "when" clause. An extension may have invoked a tool just as it was becoming disabled, and just let it go through rather than throw and break the chat. let tool = this._tools.get(dto.toolId); if (!tool) { @@ -165,12 +169,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const source = new CancellationTokenSource(); store.add(toDisposable(() => { - toolInvocation!.confirmed.complete(false); source.dispose(true); })); store.add(token.onCancellationRequested(() => { + toolInvocation?.confirmed.complete(false); source.cancel(); })); + store.add(source.token.onCancellationRequested(() => { + toolInvocation?.confirmed.complete(false); + })); token = source.token; const prepared = tool.impl.prepareToolInvocation ? @@ -179,13 +186,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); const invocationMessage = prepared?.invocationMessage ?? defaultMessage; - toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); - - model.acceptResponseProgress(request, toolInvocation); - if (prepared?.confirmationMessages) { - const userConfirmed = await toolInvocation.confirmed.p; - if (!userConfirmed) { - throw new CancellationError(); + if (tool.data.id !== 'vscode_editFile') { + toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); + model.acceptResponseProgress(request, toolInvocation); + if (prepared?.confirmationMessages) { + const userConfirmed = await toolInvocation.confirmed.p; + if (!userConfirmed) { + throw new CancellationError(); + } } } } else { diff --git a/src/vs/workbench/contrib/chat/browser/tools/tools.ts b/src/vs/workbench/contrib/chat/browser/tools/tools.ts new file mode 100644 index 00000000000..359b47eb454 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/tools/tools.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; +import { ChatModel } from '../../common/chatModel.js'; +import { IChatService } from '../../common/chatService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; + +export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.builtinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const editTool = instantiationService.createInstance(EditTool); + this._register(toolsService.registerToolData(editTool)); + this._register(toolsService.registerToolImplementation(editTool.id, editTool)); + } +} + +interface EditToolParams { + filePath: string; + explanation: string; + code: string; +} + +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 format an edit to an existing Person class: +class Person { + // ...existing code... + age: number; + // ...existing code... + getAge() { + return this.age; + } +} +`; + +class EditTool implements IToolData, IToolImpl { + readonly id = 'vscode_editFile'; + readonly tags = ['vscode_editing']; + readonly displayName = localize('chat.tools.editFile', "Edit File"); + readonly 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. ${codeInstructions}`; + readonly inputSchema: IJSONSchema; + + constructor( + @IChatService private readonly chatService: IChatService, + @IChatEditingService private readonly chatEditingService: IChatEditingService, + @ICodeMapperService private readonly codeMapperService: ICodeMapperService + ) { + this.inputSchema = { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'An absolute path to the file to edit', + }, + explanation: { + type: 'string', + description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', + }, + code: { + type: 'string', + description: 'The code change to apply to the file. ' + codeInstructions + } + }, + required: ['filePath', 'explanation', 'code'] + }; + } + + 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; + if (!parameters.filePath || !parameters.explanation || !parameters.code) { + throw new Error(`Invalid tool input: ${JSON.stringify(parameters)}`); + } + + const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; + const request = model.getRequests().at(-1)!; + + const uri = URI.file(parameters.filePath); + 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') + }); + + if (this.chatEditingService.currentEditingSession?.chatSessionId !== model.sessionId) { + 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 }], + conversation: [] + }, { + textEdit: (target, edits) => { + model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); + } + }, token); + + model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); + + if (result?.errorMessage) { + throw new Error(result.errorMessage); + } + + await new Promise((resolve) => { + autorun((r) => { + const currentEditingSession = this.chatEditingService.currentEditingSessionObs.read(r); + const entries = currentEditingSession?.entries.read(r); + const currentFile = entries?.find((e) => e.modifiedURI.toString() === uri.toString()); + if (currentFile && !currentFile.isCurrentlyBeingModified.read(r)) { + resolve(true); + } + }); + }); + + return { + content: [{ kind: 'text', value: 'Success' }] + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index e8d88c92598..8045fc3d091 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -70,6 +70,8 @@ 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; metadata: IChatAgentMetadata; @@ -201,9 +203,11 @@ export interface IChatAgentCompletionItem { export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed IChatAgent + * undefined when an agent was removed */ readonly onDidChangeAgents: Event; + readonly toolsAgentModeEnabled: boolean; + toggleToolsAgentMode(): void; registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; @@ -235,6 +239,8 @@ export interface IChatAgentService { updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } +const ChatToolsAgentModeStorageKey = 'chat.toolsAgentMode'; + export class ChatAgentService extends Disposable implements IChatAgentService { public static readonly AGENT_LEADER = '@'; @@ -250,11 +256,14 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _hasDefaultAgent: IContextKey; private readonly _defaultAgentRegistered: IContextKey; private readonly _editingAgentRegistered: IContextKey; + private readonly _agentModeContextKey: IContextKey; + private readonly _hasToolsAgentContextKey: IContextKey; private _chatParticipantDetectionProviders = new Map(); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService); @@ -265,6 +274,13 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._updateContextKeys(); } })); + + this._agentModeContextKey = ChatContextKeys.Editing.agentMode.bindTo(contextKeyService); + this._hasToolsAgentContextKey = ChatContextKeys.Editing.hasToolsAgent.bindTo(contextKeyService); + this._agentModeContextKey.set( + this.storageService.getBoolean(ChatToolsAgentModeStorageKey, StorageScope.WORKSPACE, false)); + this._register( + this.storageService.onWillSaveState(() => this.storageService.store(ChatToolsAgentModeStorageKey, this._agentModeContextKey.get(), StorageScope.WORKSPACE, StorageTarget.USER))); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -311,15 +327,23 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private _updateContextKeys(): void { let editingAgentRegistered = 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) { + toolsAgentRegistered = 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)); + } } registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { @@ -384,7 +408,22 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { - return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); + return findLast(this.getActivatedAgents(), a => { + if (location === ChatAgentLocation.EditingSession && this.toolsAgentModeEnabled !== !!a.isToolsAgent) { + return false; + } + + return !!a.isDefault && a.locations.includes(location); + }); + } + + public get toolsAgentModeEnabled(): boolean { + return !!this._hasToolsAgentContextKey.get() && !!this._agentModeContextKey.get(); + } + + toggleToolsAgentMode(): void { + this._agentModeContextKey.set(!this._agentModeContextKey.get()); + this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { @@ -404,8 +443,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return this._agents.get(id)?.data; } - private _agentIsEnabled(id: string): boolean { - const entry = this._agents.get(id); + private _agentIsEnabled(idOrAgent: string | IChatAgentEntry): boolean { + const entry = typeof idOrAgent === 'string' ? this._agents.get(idOrAgent) : idOrAgent; return !entry?.data.when || this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(entry.data.when)); } @@ -554,6 +593,7 @@ 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 metadata(): IChatAgentMetadata { return this.data.metadata; } get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } get locations(): ChatAgentLocation[] { return this.data.locations; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index be03126dd2d..68c50c90600 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -76,4 +76,9 @@ export namespace ChatContextKeys { 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.") }), + agentMode: new RawContextKey('chatAgentMode', false, { type: 'boolean', description: localize('chatEditingAgentMode', "True when edits is in agent mode.") }), + }; } diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 211d7e7e9c9..401211b3c4f 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -22,6 +22,7 @@ export interface IRawChatParticipantContribution { when?: string; description?: string; isDefault?: boolean; + isAgent?: boolean; isSticky?: boolean; sampleRequest?: string; commands?: IRawChatCommandContribution[]; diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 7632ee2f0b6..76e572c91df 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -48,10 +48,6 @@ export class ChatToolInvocation implements IChatToolInvocation { this._confirmDeferred.p.then(confirmed => { this._isConfirmed = confirmed; this._confirmationMessages = undefined; - if (!confirmed) { - // Spinner -> check - this._isCompleteDeferred.complete(); - } }); this._isCompleteDeferred.p.then(() => { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index b7cd3f36141..b078e519666 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -147,7 +147,7 @@ export interface IChatCodeCitations { export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations; export interface IChatLiveUpdateData { - firstWordTime: number; + totalTime: number; lastUpdateTime: number; impliedWordLoadRate: number; lastWordCount: number; @@ -566,10 +566,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi if (!_model.isComplete) { this._contentUpdateTimings = { - firstWordTime: 0, + totalTime: 0, lastUpdateTime: Date.now(), impliedWordLoadRate: 0, - lastWordCount: 0 + lastWordCount: 0, }; } @@ -579,12 +579,14 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi const now = Date.now(); const wordCount = countWords(_model.response.getMarkdown()); - // Apply a min time difference, or the rate is typically too high for first few words - const timeDiff = Math.max(now - this._contentUpdateTimings.firstWordTime, 250); - const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); + const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 1000); + const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); + const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (newTotalTime / 1000); + this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); this._contentUpdateTimings = { - firstWordTime: this._contentUpdateTimings.firstWordTime === 0 && this.response.value.some(v => v.kind === 'markdownContent') ? now : this._contentUpdateTimings.firstWordTime, + totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? + newTotalTime : + this._contentUpdateTimings.totalTime, lastUpdateTime: now, impliedWordLoadRate, lastWordCount: wordCount diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index cec803118e1..580fa44ce45 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -9,6 +9,7 @@ import { ContextKeyExpression } from '../../../../../platform/contextkey/common/ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../common/chatAgents.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { @@ -41,7 +42,7 @@ suite('ChatAgents', function () { let contextKeyService: TestingContextKeyService; setup(() => { contextKeyService = new TestingContextKeyService(); - chatAgentService = store.add(new ChatAgentService(contextKeyService)); + chatAgentService = store.add(new ChatAgentService(contextKeyService, store.add(new TestStorageService()))); }); test('registerAgent', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 60e55441ce7..f84b3ba640b 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -64,15 +64,6 @@ suite('VoiceChat', () => { ]; class TestChatAgentService implements IChatAgentService { - hasChatParticipantDetectionProviders(): boolean { - throw new Error('Method not implemented.'); - } - registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { - throw new Error('Method not implemented.'); - } - detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { - throw new Error('Method not implemented.'); - } _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } @@ -93,6 +84,19 @@ suite('VoiceChat', () => { getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + readonly toolsAgentModeEnabled: boolean = false; + toggleToolsAgentMode(): void { + throw new Error('Method not implemented.'); + } + hasChatParticipantDetectionProviders(): boolean { + throw new Error('Method not implemented.'); + } + registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable { + throw new Error('Method not implemented.'); + } + detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> { + throw new Error('Method not implemented.'); + } } class TestSpeechService implements ISpeechService { From 47e4f1c45c8c70811d1ad7fcf8b22e781788f177 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 Jan 2025 08:26:58 +0100 Subject: [PATCH 24/27] Need to be clear on what the 30d free trial is when hitting quota (fix microsoft/vscode-copilot-release#3658) (#237374) --- src/vs/workbench/contrib/chat/browser/chatQuotasService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 62bf59aca5e..6e2ab0a5b34 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -123,7 +123,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService message = localize('chatAndCompletionsQuotaExceeded', "You've reached the limit of the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(that.quotas.quotaResetDate)); } - const upgradeToPro = localize('upgradeToPro', "Here's what you can expect when upgrading to Copilot Pro:\n- Unlimited code completions\n- Unlimited chat messages\n- 30-day free trial"); + 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"); await dialogService.prompt({ type: 'none', From 3958e26f650620710d7bc02e70a580cc18c0cbc5 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 7 Jan 2025 10:00:03 +0100 Subject: [PATCH 25/27] Enable edit context (#235386) * Revert "Revert Enablement of EditContext on Insiders (#235062)" This reverts commit 45385e1c6f93a89a7992c0baacac62792434adac. * adding product import --- src/typings/editContext.d.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 3 +- .../services/driver/browser/driver.ts | 40 ++++++++++++++----- test/automation/src/code.ts | 9 +++-- test/automation/src/debug.ts | 9 +++-- test/automation/src/editor.ts | 15 ++++--- test/automation/src/editors.ts | 3 +- test/automation/src/extensions.ts | 3 +- test/automation/src/notebook.ts | 7 ++-- test/automation/src/scm.ts | 16 +++++--- test/automation/src/settings.ts | 14 +++++-- 11 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 5b5da0ac7e9..09585848667 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 5fe2028e366..1847c2ff345 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../platform/product/common/product.js'; //#region typed options @@ -5822,7 +5823,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false, + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa973..ad689b9db6f 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,36 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); + element.value = newValue; + element.setSelectionRange(newStart, newStart); - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index a925cdd65bc..fd64b7cdeb1 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -28,6 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +79,7 @@ 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); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +98,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index b7b7d427f4b..e2e227fc35e 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index 538866bfc06..dd616079565 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,18 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index b3a914ffff0..472385c8534 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 2a481f9fe76..c881e4fd8dc 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index dff250027db..cd46cbdb0dd 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 9f950f2b16a..6489badbe8a 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 68401eb0eda..8cf221b1487 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -57,13 +59,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +73,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } From 8c37a6903f3219995e6f763cc3cdc6ab8c775f4a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 7 Jan 2025 10:03:20 +0100 Subject: [PATCH 26/27] Preserve user selected language mode when renaming a file (fix #203648) (#237382) --- .../common/editor/textEditorModel.ts | 31 +++++++++--- .../common/textFileEditorModelManager.ts | 47 ++++++++++++++----- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 482364fda50..53b900ec6ed 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -75,13 +75,22 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel return true; } - private _hasLanguageSetExplicitly: boolean = false; - get hasLanguageSetExplicitly(): boolean { return this._hasLanguageSetExplicitly; } + private _blockLanguageChangeListener = false; + private _languageChangeSource: 'user' | 'api' | undefined = undefined; + get languageChangeSource() { return this._languageChangeSource; } + get hasLanguageSetExplicitly() { + // This is technically not 100% correct, because 'api' can also be + // set as source if a model is resolved as text first and then + // transitions into the resolved language. But to preserve the current + // behaviour, we do not change this property. Rather, `languageChangeSource` + // can be used to get more fine grained information. + return typeof this._languageChangeSource === 'string'; + } setLanguageId(languageId: string, source?: string): void { // Remember that an explicit language was set - this._hasLanguageSetExplicitly = true; + this._languageChangeSource = 'user'; this.setLanguageIdInternal(languageId, source); } @@ -95,18 +104,26 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel return; } - this.textEditorModel.setLanguage(this.languageService.createById(languageId), source); + this._blockLanguageChangeListener = true; + try { + this.textEditorModel.setLanguage(this.languageService.createById(languageId), source); + } finally { + this._blockLanguageChangeListener = false; + } } protected installModelListeners(model: ITextModel): void { // Setup listener for lower level language changes - const disposable = this._register(model.onDidChangeLanguage((e) => { - if (e.source === LanguageDetectionLanguageEventSource) { + const disposable = this._register(model.onDidChangeLanguage(e => { + if ( + e.source === LanguageDetectionLanguageEventSource || + this._blockLanguageChangeListener + ) { return; } - this._hasLanguageSetExplicitly = true; + this._languageChangeSource = 'api'; disposable.dispose(); })); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 8be051a5821..e48add78adb 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -26,6 +26,17 @@ import { PLAINTEXT_EXTENSION, PLAINTEXT_LANGUAGE_ID } from '../../../../editor/c import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; +interface ITextFileEditorModelToRestore { + readonly source: URI; + readonly target: URI; + readonly snapshot?: ITextSnapshot; + readonly language?: { + readonly id: string; + readonly explicit: boolean; + }; + readonly encoding?: string; +} + export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { private readonly _onDidCreate = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); @@ -171,13 +182,13 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } - private readonly mapCorrelationIdToModelsToRestore = new Map(); + private readonly mapCorrelationIdToModelsToRestore = new Map(); private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void { // Move / Copy: remember models to restore after the operation if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) { - const modelsToRestore: { source: URI; target: URI; snapshot?: ITextSnapshot; languageId?: string; encoding?: string }[] = []; + const modelsToRestore: ITextFileEditorModelToRestore[] = []; for (const { source, target } of e.files) { if (source) { @@ -210,10 +221,14 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE targetModelResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1)); } + const languageId = sourceModel.getLanguageId(); modelsToRestore.push({ source: sourceModelResource, target: targetModelResource, - languageId: sourceModel.getLanguageId(), + language: languageId ? { + id: languageId, + explicit: sourceModel.languageChangeSource === 'user' + } : undefined, encoding: sourceModel.getEncoding(), snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined }); @@ -286,16 +301,22 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE encoding: modelToRestore.encoding }); - // restore previous language only if the language is now unspecified and it was specified - // but not when the file was explicitly stored with the plain text extension - // (https://github.com/microsoft/vscode/issues/125795) - if ( - modelToRestore.languageId && - modelToRestore.languageId !== PLAINTEXT_LANGUAGE_ID && - restoredModel.getLanguageId() === PLAINTEXT_LANGUAGE_ID && - extname(target) !== PLAINTEXT_EXTENSION - ) { - restoredModel.updateTextEditorModel(undefined, modelToRestore.languageId); + // restore model language only if it is specific + if (modelToRestore.language?.id && modelToRestore.language.id !== PLAINTEXT_LANGUAGE_ID) { + + // an explicitly set language is restored via `setLanguageId` + // to preserve it as explicitly set by the user. + // (https://github.com/microsoft/vscode/issues/203648) + if (modelToRestore.language.explicit) { + restoredModel.setLanguageId(modelToRestore.language.id); + } + + // otherwise, a model language is applied via lower level + // APIs to not confuse it with an explicitly set language. + // (https://github.com/microsoft/vscode/issues/125795) + else if (restoredModel.getLanguageId() === PLAINTEXT_LANGUAGE_ID && extname(target) !== PLAINTEXT_EXTENSION) { + restoredModel.updateTextEditorModel(undefined, modelToRestore.language.id); + } } })); } From 12c1d4fb1753aeda4b55de73b8a8ee58c607d780 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 7 Jan 2025 10:08:23 +0100 Subject: [PATCH 27/27] milestone update (#237383) --- .vscode/notebooks/api.github-issues | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 0423e9e3afc..d29f2bc441d 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"November 2024\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"January 2025\"" }, { "kind": 1,