diff --git a/.npmrc b/.npmrc index 242cb16b7c2..05f84f8a2ad 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="34.4.1" -ms_build_id="11317338" +target="34.5.1" +ms_build_id="11369351" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.nvmrc b/.nvmrc index 0254b1e633c..5bd6811705e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.18.2 +20.19.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index f4620a0045e..ba659d5ac8b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -172,9 +172,9 @@ "css.format.spaceAroundSelectorSeparator": true, "eslint.useFlatConfig": true, "editor.occurrencesHighlightDelay": 0, - "editor.experimental.preferTreeSitter.typescript": true, - "editor.experimental.preferTreeSitter.regex": true, - "editor.experimental.preferTreeSitter.css": true, + // "editor.experimental.preferTreeSitter.typescript": true, + // "editor.experimental.preferTreeSitter.regex": true, + // "editor.experimental.preferTreeSitter.css": true, "typescript.experimental.expandableHover": true, "git.diagnosticsCommitHook.enabled": true, "git.diagnosticsCommitHook.sources": { diff --git a/build/azure-pipelines/common/codesign.js b/build/azure-pipelines/common/codesign.js new file mode 100644 index 00000000000..e3a8f330dcd --- /dev/null +++ b/build/azure-pipelines/common/codesign.js @@ -0,0 +1,30 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.printBanner = printBanner; +exports.streamProcessOutputAndCheckResult = streamProcessOutputAndCheckResult; +exports.spawnCodesignProcess = spawnCodesignProcess; +const zx_1 = require("zx"); +function printBanner(title) { + title = `${title} (${new Date().toISOString()})`; + console.log('\n'); + console.log('#'.repeat(75)); + console.log(`# ${title.padEnd(71)} #`); + console.log('#'.repeat(75)); + console.log('\n'); +} +async function streamProcessOutputAndCheckResult(name, promise) { + const result = await promise.pipe(process.stdout); + if (result.ok) { + console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); + return; + } + throw new Error(`${name} failed: ${result.stderr}`); +} +function spawnCodesignProcess(esrpCliDLLPath, type, folder, glob) { + return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; +} +//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/codesign.ts b/build/azure-pipelines/common/codesign.ts new file mode 100644 index 00000000000..9f26b3924b5 --- /dev/null +++ b/build/azure-pipelines/common/codesign.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, ProcessPromise } from 'zx'; + +export function printBanner(title: string) { + title = `${title} (${new Date().toISOString()})`; + + console.log('\n'); + console.log('#'.repeat(75)); + console.log(`# ${title.padEnd(71)} #`); + console.log('#'.repeat(75)); + console.log('\n'); +} + +export async function streamProcessOutputAndCheckResult(name: string, promise: ProcessPromise): Promise { + const result = await promise.pipe(process.stdout); + if (result.ok) { + console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); + return; + } + + throw new Error(`${name} failed: ${result.stderr}`); +} + +export function spawnCodesignProcess(esrpCliDLLPath: string, type: 'sign-windows' | 'sign-windows-appx' | 'sign-pgp' | 'sign-darwin' | 'notarize-darwin', folder: string, glob: string): ProcessPromise { + return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; +} diff --git a/build/azure-pipelines/darwin/codesign.js b/build/azure-pipelines/darwin/codesign.js index 3be56fdee7e..edc3a5f6f80 100644 --- a/build/azure-pipelines/darwin/codesign.js +++ b/build/azure-pipelines/darwin/codesign.js @@ -4,25 +4,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -const zx_1 = require("zx"); +const codesign_1 = require("../common/codesign"); const publish_1 = require("../common/publish"); -function printBanner(title) { - title = `${title} (${new Date().toISOString()})`; - console.log('\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n'); -} -async function handleProcessPromise(name, promise) { - const result = await promise.pipe(process.stdout); - if (!result.ok) { - throw new Error(`${name} failed: ${result.stderr}`); - } -} -function sign(esrpCliDLLPath, type, folder, glob) { - return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} '${glob}'`; -} async function main() { const arch = (0, publish_1.e)('VSCODE_ARCH'); const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); @@ -30,18 +13,18 @@ async function main() { const folder = `${pipelineWorkspace}/unsigned_vscode_client_darwin_${arch}_archive`; const glob = `VSCode-darwin-${arch}.zip`; // Codesign - printBanner('Codesign'); - const codeSignTask = sign(esrpCliDLLPath, 'sign-darwin', folder, glob); - await handleProcessPromise('Codesign', codeSignTask); + (0, codesign_1.printBanner)('Codesign'); + const codeSignTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-darwin', folder, glob); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign', codeSignTask); // Notarize - printBanner('Notarize'); - const notarizeTask = sign(esrpCliDLLPath, 'notarize-darwin', folder, glob); - await handleProcessPromise('Notarize', notarizeTask); + (0, codesign_1.printBanner)('Notarize'); + const notarizeTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'notarize-darwin', folder, glob); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Notarize', notarizeTask); } main().then(() => { process.exit(0); }, err => { - console.error(err); + console.error(`ERROR: ${err}`); process.exit(1); }); //# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/darwin/codesign.ts b/build/azure-pipelines/darwin/codesign.ts index 329f5380538..a9de0206d6e 100644 --- a/build/azure-pipelines/darwin/codesign.ts +++ b/build/azure-pipelines/darwin/codesign.ts @@ -3,30 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, ProcessPromise } from 'zx'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; import { e } from '../common/publish'; -function printBanner(title: string) { - title = `${title} (${new Date().toISOString()})`; - - console.log('\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n'); -} - -async function handleProcessPromise(name: string, promise: ProcessPromise): Promise { - const result = await promise.pipe(process.stdout); - if (!result.ok) { - throw new Error(`${name} failed: ${result.stderr}`); - } -} - -function sign(esrpCliDLLPath: string, type: 'sign-darwin' | 'notarize-darwin', folder: string, glob: string): ProcessPromise { - return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} '${glob}'`; -} - async function main() { const arch = e('VSCODE_ARCH'); const esrpCliDLLPath = e('EsrpCliDllPath'); @@ -37,18 +16,18 @@ async function main() { // Codesign printBanner('Codesign'); - const codeSignTask = sign(esrpCliDLLPath, 'sign-darwin', folder, glob); - await handleProcessPromise('Codesign', codeSignTask); + const codeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', folder, glob); + await streamProcessOutputAndCheckResult('Codesign', codeSignTask); // Notarize printBanner('Notarize'); - const notarizeTask = sign(esrpCliDLLPath, 'notarize-darwin', folder, glob); - await handleProcessPromise('Notarize', notarizeTask); + const notarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', folder, glob); + await streamProcessOutputAndCheckResult('Notarize', notarizeTask); } main().then(() => { process.exit(0); }, err => { - console.error(err); + console.error(`ERROR: ${err}`); process.exit(1); }); diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 64303e5aad6..a6072c8f4fa 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -264,10 +264,7 @@ steps: echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" displayName: Find ESRP CLI - - pwsh: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/darwin/codesign.js } + - script: npx deemon --detach --wait node build/azure-pipelines/darwin/codesign.js env: EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) @@ -285,10 +282,7 @@ steps: PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - - pwsh: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npx deemon --attach -- npx zx build/azure-pipelines/darwin/codesign.js } + - script: npx deemon --attach node build/azure-pipelines/darwin/codesign.js condition: succeededOrFailed() displayName: "Post-job: ✍️ Codesign & Notarize" diff --git a/build/azure-pipelines/linux/codesign.js b/build/azure-pipelines/linux/codesign.js index e52d8797a55..98b97db5666 100644 --- a/build/azure-pipelines/linux/codesign.js +++ b/build/azure-pipelines/linux/codesign.js @@ -4,40 +4,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -const zx_1 = require("zx"); +const codesign_1 = require("../common/codesign"); const publish_1 = require("../common/publish"); -function printBanner(title) { - title = `${title} (${new Date().toISOString()})`; - console.log('\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n'); -} -async function streamTaskOutputAndCheckResult(name, promise) { - const result = await promise.pipe(process.stdout); - if (result.ok) { - console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); - return; - } - throw new Error(`${name} failed: ${result.stderr}`); -} -function sign(esrpCliDLLPath, type, folder, glob) { - return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; -} async function main() { const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); // Start the code sign processes in parallel // 1. Codesign deb package // 2. Codesign rpm package - const codesignTask1 = sign(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); - const codesignTask2 = sign(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); + const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); + const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); // Codesign deb package - printBanner('Codesign deb package'); - await streamTaskOutputAndCheckResult('Codesign deb package', codesignTask1); + (0, codesign_1.printBanner)('Codesign deb package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign deb package', codesignTask1); // Codesign rpm package - printBanner('Codesign rpm package'); - await streamTaskOutputAndCheckResult('Codesign rpm package', codesignTask2); + (0, codesign_1.printBanner)('Codesign rpm package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign rpm package', codesignTask2); } main().then(() => { process.exit(0); diff --git a/build/azure-pipelines/linux/codesign.ts b/build/azure-pipelines/linux/codesign.ts index 49cd0756bf3..1f74cc21ee9 100644 --- a/build/azure-pipelines/linux/codesign.ts +++ b/build/azure-pipelines/linux/codesign.ts @@ -3,49 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, ProcessPromise } from 'zx'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; import { e } from '../common/publish'; -function printBanner(title: string) { - title = `${title} (${new Date().toISOString()})`; - - console.log('\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n'); -} - -async function streamTaskOutputAndCheckResult(name: string, promise: ProcessPromise): Promise { - const result = await promise.pipe(process.stdout); - if (result.ok) { - console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); - return; - } - - throw new Error(`${name} failed: ${result.stderr}`); -} - -function sign(esrpCliDLLPath: string, type: 'sign-pgp', folder: string, glob: string): ProcessPromise { - return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; -} - async function main() { const esrpCliDLLPath = e('EsrpCliDllPath'); // Start the code sign processes in parallel // 1. Codesign deb package // 2. Codesign rpm package - const codesignTask1 = sign(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); - const codesignTask2 = sign(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); + const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); + const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); // Codesign deb package printBanner('Codesign deb package'); - await streamTaskOutputAndCheckResult('Codesign deb package', codesignTask1); + await streamProcessOutputAndCheckResult('Codesign deb package', codesignTask1); // Codesign rpm package printBanner('Codesign rpm package'); - await streamTaskOutputAndCheckResult('Codesign rpm package', codesignTask2); + await streamProcessOutputAndCheckResult('Codesign rpm package', codesignTask2); } main().then(() => { diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index f50ceb8d842..453849168c8 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -263,7 +263,8 @@ extends: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - job: CLIMacOSX64 pool: - name: AcesShared + name: Azure Pipelines + image: macOS-13 os: macOS variables: # todo@connor4312 to diagnose build flakes @@ -279,7 +280,8 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - job: CLIMacOSARM64 pool: - name: AcesShared + name: Azure Pipelines + image: macOS-13 os: macOS variables: # todo@connor4312 to diagnose build flakes diff --git a/build/azure-pipelines/win32/codesign.js b/build/azure-pipelines/win32/codesign.js index de4bcf83117..630f9a64ba1 100644 --- a/build/azure-pipelines/win32/codesign.js +++ b/build/azure-pipelines/win32/codesign.js @@ -5,41 +5,32 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const zx_1 = require("zx"); -const arch = process.env['VSCODE_ARCH']; -const esrpCliDLLPath = process.env['EsrpCliDllPath']; -const codeSigningFolderPath = process.env['CodeSigningFolderPath']; -function printBanner(title) { - title = `${title} (${new Date().toISOString()})`; - console.log('\n\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n\n'); -} -function sign(type, glob) { - return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${codeSigningFolderPath} '${glob}'`; -} +const codesign_1 = require("../common/codesign"); +const publish_1 = require("../common/publish"); async function main() { (0, zx_1.usePwsh)(); + const arch = (0, publish_1.e)('VSCODE_ARCH'); + const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); + const codeSigningFolderPath = (0, publish_1.e)('CodeSigningFolderPath'); // Start the code sign processes in parallel // 1. Codesign executables and shared libraries // 2. Codesign Powershell scripts // 3. Codesign context menu appx package (insiders only) - const codesignTask1 = sign('sign-windows', '*.dll,*.exe,*.node'); - const codesignTask2 = sign('sign-windows-appx', '*.ps1'); + const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); + const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' - ? sign('sign-windows-appx', '*.appx') + ? (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') : undefined; // Codesign executables and shared libraries - printBanner('Codesign executables and shared libraries'); - await codesignTask1.pipe(process.stdout); + (0, codesign_1.printBanner)('Codesign executables and shared libraries'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign executables and shared libraries', codesignTask1); // Codesign Powershell scripts - printBanner('Codesign Powershell scripts'); - await codesignTask2.pipe(process.stdout); + (0, codesign_1.printBanner)('Codesign Powershell scripts'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign Powershell scripts', codesignTask2); if (codesignTask3) { // Codesign context menu appx package - printBanner('Codesign context menu appx package'); - await codesignTask3.pipe(process.stdout); + (0, codesign_1.printBanner)('Codesign context menu appx package'); + await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign context menu appx package', codesignTask3); } // Create build artifact directory await (0, zx_1.$) `New-Item -ItemType Directory -Path .build/win32-${arch} -Force`; @@ -47,30 +38,36 @@ async function main() { if (process.env['BUILT_CLIENT']) { // Product version const version = await (0, zx_1.$) `node -p "require('../VSCode-win32-${arch}/resources/app/package.json').version"`; - printBanner('Package client'); + (0, codesign_1.printBanner)('Package client'); const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}-${version}.zip`; await (0, zx_1.$) `7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); await (0, zx_1.$) `7z.exe l ${clientArchivePath}`.pipe(process.stdout); } // Package server if (process.env['BUILT_SERVER']) { - printBanner('Package server'); + (0, codesign_1.printBanner)('Package server'); const serverArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}.zip`; await (0, zx_1.$) `7z.exe a -tzip ${serverArchivePath} ../vscode-server-win32-${arch}`.pipe(process.stdout); await (0, zx_1.$) `7z.exe l ${serverArchivePath}`.pipe(process.stdout); } // Package server (web) if (process.env['BUILT_WEB']) { - printBanner('Package server (web)'); + (0, codesign_1.printBanner)('Package server (web)'); const webArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}-web.zip`; await (0, zx_1.$) `7z.exe a -tzip ${webArchivePath} ../vscode-server-win32-${arch}-web`.pipe(process.stdout); await (0, zx_1.$) `7z.exe l ${webArchivePath}`.pipe(process.stdout); } // Sign setup if (process.env['BUILT_CLIENT']) { - printBanner('Sign setup packages (system, user)'); - await (0, zx_1.$) `npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`.pipe(process.stdout); + (0, codesign_1.printBanner)('Sign setup packages (system, user)'); + const task = (0, zx_1.$) `npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; + await (0, codesign_1.streamProcessOutputAndCheckResult)('Sign setup packages (system, user)', task); } } -main(); +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); //# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/win32/codesign.ts b/build/azure-pipelines/win32/codesign.ts index b75a4d2a2d2..7e7170709b5 100644 --- a/build/azure-pipelines/win32/codesign.ts +++ b/build/azure-pipelines/win32/codesign.ts @@ -3,51 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, ProcessPromise, usePwsh } from 'zx'; - -const arch = process.env['VSCODE_ARCH']; -const esrpCliDLLPath = process.env['EsrpCliDllPath']; -const codeSigningFolderPath = process.env['CodeSigningFolderPath']; - -function printBanner(title: string) { - title = `${title} (${new Date().toISOString()})`; - - console.log('\n\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n\n'); -} - -function sign(type: 'sign-windows' | 'sign-windows-appx', glob: string): ProcessPromise { - return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${codeSigningFolderPath} '${glob}'`; -} +import { $, usePwsh } from 'zx'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; +import { e } from '../common/publish'; async function main() { usePwsh(); + const arch = e('VSCODE_ARCH'); + const esrpCliDLLPath = e('EsrpCliDllPath'); + const codeSigningFolderPath = e('CodeSigningFolderPath'); + // Start the code sign processes in parallel // 1. Codesign executables and shared libraries // 2. Codesign Powershell scripts // 3. Codesign context menu appx package (insiders only) - const codesignTask1 = sign('sign-windows', '*.dll,*.exe,*.node'); - const codesignTask2 = sign('sign-windows-appx', '*.ps1'); + const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); + const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' - ? sign('sign-windows-appx', '*.appx') + ? spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') : undefined; // Codesign executables and shared libraries printBanner('Codesign executables and shared libraries'); - await codesignTask1.pipe(process.stdout); + await streamProcessOutputAndCheckResult('Codesign executables and shared libraries', codesignTask1); // Codesign Powershell scripts printBanner('Codesign Powershell scripts'); - await codesignTask2.pipe(process.stdout); + await streamProcessOutputAndCheckResult('Codesign Powershell scripts', codesignTask2); if (codesignTask3) { // Codesign context menu appx package printBanner('Codesign context menu appx package'); - await codesignTask3.pipe(process.stdout); + await streamProcessOutputAndCheckResult('Codesign context menu appx package', codesignTask3); } // Create build artifact directory @@ -83,8 +71,14 @@ async function main() { // Sign setup if (process.env['BUILT_CLIENT']) { printBanner('Sign setup packages (system, user)'); - await $`npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`.pipe(process.stdout); + const task = $`npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; + await streamProcessOutputAndCheckResult('Sign setup packages (system, user)', task); } } -main(); +main().then(() => { + process.exit(0); +}, err => { + console.error(`ERROR: ${err}`); + process.exit(1); +}); diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index e1773ebb553..9bfbc95a4c8 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -5b3ba7acb5eabf1a8651322138094ab26d313171f624456cf4b6243c74d35bc4 *chromedriver-v34.4.1-darwin-arm64.zip -e16db516f323e30dc6fd54cac11f72bfdc8fe7852839b55a3609b30fee2318f0 *chromedriver-v34.4.1-darwin-x64.zip -1778d1c3903beaa33d75c82cb9b6769f2b303bd75c991f6e55382d916055efd4 *chromedriver-v34.4.1-linux-arm64.zip -41b9afe8dbe5a0656d6e395ed0d00a26e5e6206a7399768066d91501f181e0d5 *chromedriver-v34.4.1-linux-armv7l.zip -e49534cc2c68f8aa58df6f1f2c1706b579b54f9ae362dba6a04469c04f1da9f3 *chromedriver-v34.4.1-linux-x64.zip -e9cffc5696ba3c80810c7e87d7a7aa1c5239d6504a8d72b3d4e58fbb6d6e50a3 *chromedriver-v34.4.1-mas-arm64.zip -5fa81bec24071190875060edfe181aa78eca2e8d121f0e3943c12de65d62f20a *chromedriver-v34.4.1-mas-x64.zip -e7ffa8bf700bf2c635a1c641412fd5fa7f50a1efa6ff95e3c8b273e65e8eb9ff *chromedriver-v34.4.1-win32-arm64.zip -eb17200100de2579af06dfd120d7e5f6915eca95b057c69a70d504ff44b7d06f *chromedriver-v34.4.1-win32-ia32.zip -e4ff12f8672af8e043ba60b5fc6b951eed3da8cba0ca248688b32558ffe3e9f6 *chromedriver-v34.4.1-win32-x64.zip -da620386690d47805ca1f61d3d48756b2e7cf9d8f5f439da00c8b3bff36d0932 *electron-api.json -f3eb8f60ab6431989680e89e458ed31706d522d7d5a17140c7eeec9ac1600bf4 *electron-v34.4.1-darwin-arm64-dsym-snapshot.zip -f483a926c0fd965e77e55a2b9588b482885dc24ccbdf559a9708203c897714a5 *electron-v34.4.1-darwin-arm64-dsym.zip -06078a0d115ce8e4ec556a8b084865343ef9522e6ec63635a97791a3c943ec81 *electron-v34.4.1-darwin-arm64-symbols.zip -5a142772493b25ad22dda774a1d4da78887024adae8e83b0e74ad0ba64a7f55a *electron-v34.4.1-darwin-arm64.zip -da815e7668d6f5ea87723f06283e436a8d3341b800b7f3f70a92af4e73401f7a *electron-v34.4.1-darwin-x64-dsym-snapshot.zip -ccd0d6135d6726f31d2d1315e56c50d221040542a2aa92634a7f006fcb1bca2b *electron-v34.4.1-darwin-x64-dsym.zip -9c3f151841fe311a69ad90a7258585fdaa76a18f56a9d46e0a9c0d6a57a86ba4 *electron-v34.4.1-darwin-x64-symbols.zip -618156b4c923adcc2bf3d0a81d82dc874f27129893b7b3c349d0ca13651619e8 *electron-v34.4.1-darwin-x64.zip -9a54e37daae12d5bbbac5cc5feb9393733164a8e2acb70063bbb33122bfad598 *electron-v34.4.1-linux-arm64-debug.zip -d1a4d53143bb2cd9b4bb9aa0355b0b2123a336c0835cbed2f667dc8ba9db925e *electron-v34.4.1-linux-arm64-symbols.zip -d22f1778894393414d7da01aa3f85d6f11f2cb5a5c7623d9d8339bcd824df4cb *electron-v34.4.1-linux-arm64.zip -9a54e37daae12d5bbbac5cc5feb9393733164a8e2acb70063bbb33122bfad598 *electron-v34.4.1-linux-armv7l-debug.zip -589b23faf458b73cff2d05ca6ebe36dde01999aac1ec17667ac0c0fa108a0631 *electron-v34.4.1-linux-armv7l-symbols.zip -29af72e24c74da70c85bfdce1ed6492b7efbe85f88cfb3da642844b51e5d7259 *electron-v34.4.1-linux-armv7l.zip -a994667229d35afd29c77df947bdf506b6224de6ad8d0b4146bce13e42d92631 *electron-v34.4.1-linux-x64-debug.zip -befc79c7f87b39d97dbb68f9b98744585cb6408b9c9e0f9649a002349beeb832 *electron-v34.4.1-linux-x64-symbols.zip -18ebcf0d2b681e273eb003ea0d77bb4fb91ed891f39778ad9c22b41972ed1975 *electron-v34.4.1-linux-x64.zip -ddb7e381435cfd0b0b6adc449921ecc5564c43edce049a90c951638a97c84455 *electron-v34.4.1-mas-arm64-dsym-snapshot.zip -0fab6ac44f42061dc267f4cb5707f9a49348f600bb1f882a3293adc1c7245a72 *electron-v34.4.1-mas-arm64-dsym.zip -d56d3956fde4cc745a2dcea9b60ceebe5cb660d7c44e8b57b865ee6e5aff19dc *electron-v34.4.1-mas-arm64-symbols.zip -797a4ea8b9d933f65db856e80f57cf0ce0d243f28d0dc7d3ffd98f5b8615e03f *electron-v34.4.1-mas-arm64.zip -82929e94e7cc21b38e14ff807aa26ef9e20b7de1b94fb227247fc92856d491f0 *electron-v34.4.1-mas-x64-dsym-snapshot.zip -2b4fd9bc83edfdff998caad169674cf210a3dcc39ba15c7e576e8457591e022c *electron-v34.4.1-mas-x64-dsym.zip -bc9af8e1e8f7b91ec23f3d3f2eb10a291e07af4b925b3b85a5beb83ab333006f *electron-v34.4.1-mas-x64-symbols.zip -e2a9c8674cb6f4a022a6aa15a739f3a6cb0ad07ceccb7f364518718ead2518a0 *electron-v34.4.1-mas-x64.zip -43c9eac975e60990e0d13242001a149933c62cb4f114f403a7af08a3cbbf2acb *electron-v34.4.1-win32-arm64-pdb.zip -39996830e5c8dea5f273b62b81e4b7543788640e2c1e0a82df5f5e6f5c15004d *electron-v34.4.1-win32-arm64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.1-win32-arm64-toolchain-profile.zip -c84fdea6841dc3cfc6b6b83398a80a9ea684c078ea2a2798bd62b1e31bbf26f4 *electron-v34.4.1-win32-arm64.zip -f5cc5482881d5a20226dd021379833b29b524261c07b8627346f8a0e0f32a23f *electron-v34.4.1-win32-ia32-pdb.zip -fb0106a1248f07a2f380db9dee5f6936d1319084790f479be2d5743afde784b9 *electron-v34.4.1-win32-ia32-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.1-win32-ia32-toolchain-profile.zip -f72a0622a75d6aaf1e9b1289be15aac015fed8df42504fa90f799acd513b4d7e *electron-v34.4.1-win32-ia32.zip -de5a6112e106cef1a01d78dbcb01e6e52cf11553d0438f6e07221f2b438661d3 *electron-v34.4.1-win32-x64-pdb.zip -e4560841019249f79bedebbbaeb05e79c241e6561cc36c486f450f4c2c9af8f2 *electron-v34.4.1-win32-x64-symbols.zip -c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.4.1-win32-x64-toolchain-profile.zip -dbae0c9c78480619497da03f0ac56fc972777fe6e421d5c136cf85aaf577ddd5 *electron-v34.4.1-win32-x64.zip -b6ffed59b139b21c65569d45fa082d7d95b70a6ebfdb0c894df42841b3525b3e *electron.d.ts -dc4917b118309a78e4f5a0659893cf372a5a4e6e1210e584f6d6199844780304 *ffmpeg-v34.4.1-darwin-arm64.zip -88a5b448e956d81305094c34518bb37f49c33159b6eed015d0100c29777791a1 *ffmpeg-v34.4.1-darwin-x64.zip -346101611df565cabcfaa3515b1db3f70d0891ba8f1241074dd09b69e12630d2 *ffmpeg-v34.4.1-linux-arm64.zip -5c77c712ee93bd26706daa78f0651d9b4ba8e4b46a115908f29d2742a2e1b9f0 *ffmpeg-v34.4.1-linux-armv7l.zip -f5ab70d399d528450c9499966e88ce02a368bb8c7dd7ac0676a6628fa29b3f14 *ffmpeg-v34.4.1-linux-x64.zip -57798df9d9c4afb8dde1ee09619fdd4f1f1d2c2bb727fddbc5fb0242e33f026c *ffmpeg-v34.4.1-mas-arm64.zip -7ddf75b3aedbe948ceeb865b08d488a85ba7ea36218564d89028f8e4c4bb9c3d *ffmpeg-v34.4.1-mas-x64.zip -6dc386c7dccbb73f5d359e232a81c5f50e456f30f709e41658838720a5413b99 *ffmpeg-v34.4.1-win32-arm64.zip -6dc386c7dccbb73f5d359e232a81c5f50e456f30f709e41658838720a5413b99 *ffmpeg-v34.4.1-win32-ia32.zip -6dc386c7dccbb73f5d359e232a81c5f50e456f30f709e41658838720a5413b99 *ffmpeg-v34.4.1-win32-x64.zip -1212bc0bdd47670869bd720f44effd307c085b75af0711fd0bd20ddc672556f5 *hunspell_dictionaries.zip -93bbf68ca30d4c782c6ecc62c21c69faf23e183619e2bdbed3f17e23dfbf0cea *libcxx-objects-v34.4.1-linux-arm64.zip -239225e614f705c5041766921bdcaee1fd844e2922df57560fe89d504dba6b74 *libcxx-objects-v34.4.1-linux-armv7l.zip -33991ec5a4280a849156879c3f14543105bb16ab26cf6e2fa0def1ca7ab2b63c *libcxx-objects-v34.4.1-linux-x64.zip -5d8fbcd21f1841cd2c6f02649305b31d6eafd577544621888ce82f53c92f0480 *libcxx_headers.zip -e5c18f813cc64a7d3b0404ee9adeb9cbb49e7ee5e1054b62c71fa7d1a448ad1b *libcxxabi_headers.zip -d506b629bafbb9c08cf578c4de1b336a264d2e7465bb620473b14184d2f99415 *mksnapshot-v34.4.1-darwin-arm64.zip -c655c1f6da2e9dd7128cef58641cd54429dd0519198378db61556eb0dee7c916 *mksnapshot-v34.4.1-darwin-x64.zip -e755334a81df137a3ad751fedd0b683bb931ac83b29bee246f8b3d4ba7d4cec7 *mksnapshot-v34.4.1-linux-arm64-x64.zip -85b56fcd9f00567c3074133b2c9825e8ca9d9823ff6a4624e92db400b93a37bf *mksnapshot-v34.4.1-linux-armv7l-x64.zip -f7f69137655c7c08c646c90dc5f7aa6e5ac0c370fc30d5549fc98e3ab9f9d1a7 *mksnapshot-v34.4.1-linux-x64.zip -4cbb56bcbd503ceb6ce68fa7e7d0c6e6039f71330ad5da7bc3fda7d6159a023e *mksnapshot-v34.4.1-mas-arm64.zip -d947f386ab844eca812e170c5b3ca90addb5ba4b8684db4b80e155d95196e150 *mksnapshot-v34.4.1-mas-x64.zip -4dccf62d30a1267cc849eb0a1076418e19156e6d1413a805e04de21e51367423 *mksnapshot-v34.4.1-win32-arm64-x64.zip -3bc16e32c82d29cc6524b4d0252a667d04a423a8e6048338d6685c6593fcb73f *mksnapshot-v34.4.1-win32-ia32.zip -7b8a4a6eea4703e1b8d3b9fdd5bbbb2b46039caf272d198f755e58a5a672a0f0 *mksnapshot-v34.4.1-win32-x64.zip +249f89d35cb6bd74edc07551b141bffc2045847c4cf9e57e21089d5082bdb4b4 *chromedriver-v34.5.1-darwin-arm64.zip +7ff68fd26f225deaa8c6fbcd76dc80a00f9ef73f9118075f3e2ab54dfb0c810e *chromedriver-v34.5.1-darwin-x64.zip +749f692603527e8743c81d05eb2de2e281e2b03b148ec00379f13e8da17ca7a4 *chromedriver-v34.5.1-linux-arm64.zip +14bcc062457cf31d606451aa7fae1baae720a944dead06231fe2a55f17d39966 *chromedriver-v34.5.1-linux-armv7l.zip +57cf85eb9dafe28ccdd8ba4a095cb1fd5b8c71f0743bf532b132bc45e56630ef *chromedriver-v34.5.1-linux-x64.zip +e90e10cf45f4aaba1d8b763279b7c4b85e1132bdc9faef834ffda41ee1460df8 *chromedriver-v34.5.1-mas-arm64.zip +1206e1c71ec0360be9531e48c0292ffac37e40d8d7a48dd38f1108d3d3ccc0c0 *chromedriver-v34.5.1-mas-x64.zip +1b226994cfa02663f23edfb0c8a4d3e218b7c4d037a90bbb4800a7c396b67d9f *chromedriver-v34.5.1-win32-arm64.zip +dc38291ccad6f715a82cc2ce0cfffe3bb37612fa86013d405e878ea74e4c5fb8 *chromedriver-v34.5.1-win32-ia32.zip +3ccc7e4b65adde12e26b7affeea30b9597b8841fc2a4d3c50c042e80b85853ac *chromedriver-v34.5.1-win32-x64.zip +71fe75d29208ca9e38754d903af4d5d6e80c62b04097605c36ebf722c2447842 *electron-api.json +009c833bd014b6f873974c5d3189905e705ebcb188a90ae05b60ea252319a46e *electron-v34.5.1-darwin-arm64-dsym-snapshot.zip +c5f5722c55e75e9860cb203e03626c04f30f272ef17b735946fd723600ee07ea *electron-v34.5.1-darwin-arm64-dsym.zip +06de49512ac4b0b4e374bdcd296e8c70584fb47207bb6caed9122e3cef5da8f7 *electron-v34.5.1-darwin-arm64-symbols.zip +78411442b5bd2252cf4605b6a44c35ad6a06807d03c63c61726ad7693c6d5893 *electron-v34.5.1-darwin-arm64.zip +e90b292974251336ae8990a74977065ac4dd6388836ccd1cfee3a1599a37bd39 *electron-v34.5.1-darwin-x64-dsym-snapshot.zip +35a0ac52f6036cd0a7d4bc9848477b653095b210497e36797427ff8fe3194c7f *electron-v34.5.1-darwin-x64-dsym.zip +0457bb7413c770245912342a6dd07c3588f586e8d868e0dd534179e22b07898c *electron-v34.5.1-darwin-x64-symbols.zip +8d4bc5f4495ef952589891b6c70a86d8a9d143a1d4d90d15dd81926639822031 *electron-v34.5.1-darwin-x64.zip +73be60acd1f3773f87b283eef8c26e257f16efd46a179c143311b1b9fcb4a61a *electron-v34.5.1-linux-arm64-debug.zip +53677a8f437b36b79481eb6c6f9f7557606af04ef94cce751620e8206dad36a8 *electron-v34.5.1-linux-arm64-symbols.zip +4c0d5833faa01cc3a586087b82f719c2fe331515d26bb3fa098dc79bd3ea153f *electron-v34.5.1-linux-arm64.zip +73be60acd1f3773f87b283eef8c26e257f16efd46a179c143311b1b9fcb4a61a *electron-v34.5.1-linux-armv7l-debug.zip +6eb39e79bd52f566d18a1140242c7484b89d7cd77573b92fc2e2993b51d6fbf1 *electron-v34.5.1-linux-armv7l-symbols.zip +7ed517eeaff56960a01fe53fc445e4118135eeb8267d61c37ef9df943dcc35fb *electron-v34.5.1-linux-armv7l.zip +582a2206cf1e09baa8511ca21b697cc49fddd76ef7723406b449b130b3d21730 *electron-v34.5.1-linux-x64-debug.zip +7b5d60f3d6c4ef84b0855148f14295624527cc27ab395bf54640a06eb3f7a5b0 *electron-v34.5.1-linux-x64-symbols.zip +3ae6f75fa08f5c1bdb7bbcec4dc9cf7d7f53ffcf6a4292e4a482b2ce515505e7 *electron-v34.5.1-linux-x64.zip +e6ff5c411167c0cf8c82cd737f8d0c863f4371e8e1fe213d04b502584411d239 *electron-v34.5.1-mas-arm64-dsym-snapshot.zip +8d1cb700f23d8ac7ec078d4d5d07018dfae594346e7bc5652356a5fe242a2b44 *electron-v34.5.1-mas-arm64-dsym.zip +3b74614ef81382e63f189aceb87f6c3830a23ffed046d06f672d0c1a1b361e96 *electron-v34.5.1-mas-arm64-symbols.zip +eabc29959b914f623f5f2e4011cb4e35182ed9528dc30664e59ca37c806c1d7d *electron-v34.5.1-mas-arm64.zip +ee3de3f5a96efb0197022557ec2de36d92d7423426636577864b1ae744053dea *electron-v34.5.1-mas-x64-dsym-snapshot.zip +a3db9cc489720701e3f35d2f7425c97e24f74fdb78a38bc0950b68b3f82aebb2 *electron-v34.5.1-mas-x64-dsym.zip +a9131003b1ac4a3c3327ff405e1cd8f3e61dc8a73cfae3e05cb5eb0f2d872bee *electron-v34.5.1-mas-x64-symbols.zip +1b44d42dbe9cb6bc5c2fb77f708d639e01f8ec6f74b95710fc6c8dbd70181f3b *electron-v34.5.1-mas-x64.zip +4495d8bf4d3dbb5ebc3ad135f4658e09d706368d002af9f24d236e1a0a28e994 *electron-v34.5.1-win32-arm64-pdb.zip +2c31fa61d24e736f3e327eba4d354c09471fba5aa277e215f7e2ea275b323a80 *electron-v34.5.1-win32-arm64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-arm64-toolchain-profile.zip +c0cff1c83094a430f1b202bb5035b51ebcefa54cd53d798bb63de9cb96abf223 *electron-v34.5.1-win32-arm64.zip +d662fb7afc288aa15d929fecbb391c7067448ea86b4bf01e941fa8da744a8167 *electron-v34.5.1-win32-ia32-pdb.zip +2cd1f41a3297fc271e426bd0cc5f8c3474f73438a7a303186701cb7d8b26bdb6 *electron-v34.5.1-win32-ia32-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-ia32-toolchain-profile.zip +cf86edf6cdb47d5cfb00c4eb68f7a18d70bf9e33f1f6a0481d51673cf6af7050 *electron-v34.5.1-win32-ia32.zip +9dd0e6f6ef53f8bd4d7ecd97a3bfc7e8a98de8771986071692afc57d57d199d6 *electron-v34.5.1-win32-x64-pdb.zip +f50ab96420bddd43bd5dbd56130cfcd69eea2dba18bfd3c8c3b4bb189bb033e6 *electron-v34.5.1-win32-x64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.5.1-win32-x64-toolchain-profile.zip +da606d1a085a52ddf5592110b58284fc3bf49f273f6f2e7d6a8341c98af8498e *electron-v34.5.1-win32-x64.zip +793ae7822cbdad6270c318f3c93c0e8b4f9276dea6dd87db2d1297cadc7381c6 *electron.d.ts +1d1465b4f6a3919a6e8e2fb8612b29f61b09d80f386a8fa2b806859b9be0d0bf *ffmpeg-v34.5.1-darwin-arm64.zip +e138b6422dd1648cbe817b99f59476c65ed9946d50e50094124eae660b416378 *ffmpeg-v34.5.1-darwin-x64.zip +346101611df565cabcfaa3515b1db3f70d0891ba8f1241074dd09b69e12630d2 *ffmpeg-v34.5.1-linux-arm64.zip +5c77c712ee93bd26706daa78f0651d9b4ba8e4b46a115908f29d2742a2e1b9f0 *ffmpeg-v34.5.1-linux-armv7l.zip +f5ab70d399d528450c9499966e88ce02a368bb8c7dd7ac0676a6628fa29b3f14 *ffmpeg-v34.5.1-linux-x64.zip +10e3424c01b946274fa8c651d4ea79032637feca4c8712ebb1c00f392711594f *ffmpeg-v34.5.1-mas-arm64.zip +4db0373915c2c2a055bd04755acdbcd08e00456f1fb92fefc0e05cd7fb48e4fa *ffmpeg-v34.5.1-mas-x64.zip +c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-arm64.zip +c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-ia32.zip +c8cca82fc9315f86ffb60b39e824ebec7f98361f8773ea0618d9feea92b88412 *ffmpeg-v34.5.1-win32-x64.zip +9ae3a56bf29d9704cd8cf32924aad89414f28d439e61dd54bdd8b4259b8d0b1d *hunspell_dictionaries.zip +691e23913b7dbde1f9c9b6e9f13f06353d5c7927cbab6d48b7de43e76e5eacd8 *libcxx-objects-v34.5.1-linux-arm64.zip +eaaa18779a96873daeece21c7c823d1f5d4759f8eca79dbbbf2055635df6112f *libcxx-objects-v34.5.1-linux-armv7l.zip +6a2e3dfcea9d0582ecbc2a6be124f0e830e2194111bd9aa6a9843cb956c946c4 *libcxx-objects-v34.5.1-linux-x64.zip +d4b70d94523ebd770009dba04c842450539a9bdc856de660a7391620d3bcc1fb *libcxx_headers.zip +0ed01bc1908fd8f7519ccdf636b5732c6fe2c095a6dc35a13eb6c79b9e87d7d1 *libcxxabi_headers.zip +f633cd0df0b08a15938ccdc77480bc28eb96fd85936ef76c343cc3f47fe74f3c *mksnapshot-v34.5.1-darwin-arm64.zip +a8643285a2386960ceb608ff34d6dac33942142e821e2e0c670b282389a87e53 *mksnapshot-v34.5.1-darwin-x64.zip +70863b79d4b7ab75d013a9192f7b23165e3e523b243632c7b55418767527e022 *mksnapshot-v34.5.1-linux-arm64-x64.zip +c30319434ea16416c38bbdf847432fd37fd8e1aa78c1c22b6345d02e3743c016 *mksnapshot-v34.5.1-linux-armv7l-x64.zip +e882e32b67501d36710da396167274158c1940afe67459ffa1d9df534a8f6df1 *mksnapshot-v34.5.1-linux-x64.zip +af1d08dbd3c572ae10db0d24203a28d83c87e92e966064134ec5d7770c74e3ac *mksnapshot-v34.5.1-mas-arm64.zip +238058875abebcb9233e609fadad76e85b79530f1bdfb60498b144fec82ff8fc *mksnapshot-v34.5.1-mas-x64.zip +779e494cf2088ee386bb3ffd68d5efc2de3d43e5a2e6a5a768638799c460fdab *mksnapshot-v34.5.1-win32-arm64-x64.zip +9f9790fab86209ca76ecfae3e20dc028bc0e49574872f6ac17b8856093811357 *mksnapshot-v34.5.1-win32-ia32.zip +5c39077fd59426108f15e4981c7be5ebe56aa706b9d166853225de882fee8d6e *mksnapshot-v34.5.1-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index d394605dda3..efada69028b 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -1f15b7ed18a580af31cf32bc126572292d820f547bf55bf9cdce08041a24e1d9 node-v20.18.3-darwin-arm64.tar.gz -ba668f64df9239843fefcef095ee539f5ac5aa1b0fc15a71f1ecca16abedec7a node-v20.18.3-darwin-x64.tar.gz -93a9df19238adfaa289f4784041d03edaf2fdd89fbb247faffca2fe4a1000703 node-v20.18.3-linux-arm64.tar.gz -8a84eb34287db6a273066934d7195e429f57b91686b62fc19497210204a2b3de node-v20.18.3-linux-armv7l.tar.gz -9fc3952da39b20d1fcfdb777b198cc035485afbbb1004b4df93f35245d61151e node-v20.18.3-linux-x64.tar.gz -4258e333f4b95060681d61bffa762542a8068547d3dffebe57c575b38d380dda win-arm64/node.exe -528a9aa64888a2a3ba71c6aea89434dd5ab5cb3caa9f0f31345cf5facf685ab0 win-x64/node.exe +c016cd1975a264a29dc1b07c6fbe60d5df0a0c2beb4113c0450e3d998d1a0d9c node-v20.19.0-darwin-arm64.tar.gz +a8554af97d6491fdbdabe63d3a1cfb9571228d25a3ad9aed2df856facb131b20 node-v20.19.0-darwin-x64.tar.gz +618e4294602b78e97118a39050116b70d088b16197cd3819bba1fc18b473dfc4 node-v20.19.0-linux-arm64.tar.gz +2deb2f333b42fcdeb0d215800b3d2b9af64dd88c1d0b05e67b980398d43c4dce node-v20.19.0-linux-armv7l.tar.gz +8a4dbcdd8bccef3132d21e8543940557e55dcf44f00f0a99ba8a062f4552e722 node-v20.19.0-linux-x64.tar.gz +4ec1ae34fc7c0c65b35ec3688b9dc6d8ad5feca69d5ba45f7d72d559dc850fbb win-arm64/node.exe +6e3a39787e667d50487f7335c85636c2823a53e636d73c2c841d45da4e57906c win-x64/node.exe diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index ba269af9af5..83bd45e9d3f 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -86,7 +86,12 @@ const CORE_TYPES = [ 'JsonWebKey', 'MessageEvent', // node web types + 'ReadableStream', + 'ReadableStreamReadResult', + 'ReadableStreamGenericReader', 'ReadableStreamDefaultReader', + 'value', + 'done', 'DOMException', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 39dc920a806..677b505ab22 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -86,7 +86,12 @@ const CORE_TYPES = [ 'MessageEvent', // node web types + 'ReadableStream', + 'ReadableStreamReadResult', + 'ReadableStreamGenericReader', 'ReadableStreamDefaultReader', + 'value', + 'done', 'DOMException', ]; diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 352b536194f..39cc1d12a30 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -228,15 +228,18 @@ "--vscode-editorGroupHeader-tabsBackground", "--vscode-editorGroupHeader-tabsBorder", "--vscode-editorGutter-addedBackground", + "--vscode-editorGutter-addedSecondaryBackground", "--vscode-editorGutter-background", "--vscode-editorGutter-commentGlyphForeground", "--vscode-editorGutter-commentRangeForeground", "--vscode-editorGutter-commentUnresolvedGlyphForeground", "--vscode-editorGutter-deletedBackground", + "--vscode-editorGutter-deletedSecondaryBackground", "--vscode-editorGutter-foldingControlForeground", "--vscode-editorGutter-itemBackground", "--vscode-editorGutter-itemGlyphForeground", "--vscode-editorGutter-modifiedBackground", + "--vscode-editorGutter-modifiedSecondaryBackground", "--vscode-editorHint-border", "--vscode-editorHint-foreground", "--vscode-editorHoverWidget-background", @@ -572,6 +575,8 @@ "--vscode-profileBadge-foreground", "--vscode-profiles-sashBorder", "--vscode-progressBar-background", + "--vscode-prompt-frontMatter-background", + "--vscode-prompt-frontMatter-inactiveBackground", "--vscode-quickInput-background", "--vscode-quickInput-foreground", "--vscode-quickInput-list-focusBackground", diff --git a/cgmanifest.json b/cgmanifest.json index c9c9c057fc7..6ee72b3f757 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "4819c99baa28bf2c1baf411ba100c467fec3d486" + "commitHash": "bb1a61d8737feff534bb85368dab3b7c554c863d" } }, "isOnlyProductionDependency": true, - "version": "20.18.3" + "version": "20.19.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "e87dc5707173b910d1ff4626921818aca48297c6" + "commitHash": "d0594707ded4d564c95badf5322d5893295da4ed" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.4.1" + "version": "34.5.1" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 0432c18e7b3..992e23a4410 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -536,9 +536,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -2676,9 +2676,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d" dependencies = [ "backtrace", "bytes", @@ -2696,9 +2696,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3321d581dc2..f6e2f96dbd4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,7 +16,7 @@ futures = "0.3.28" clap = { version = "4.3.0", features = ["derive", "env"] } open = "4.1.0" reqwest = { version = "0.11.22", default-features = false, features = ["json", "stream", "native-tls"] } -tokio = { version = "1.28.2", features = ["full"] } +tokio = { version = "1.38.2", features = ["full"] } tokio-util = { version = "0.7.8", features = ["compat", "codec"] } flate2 = { version = "1.0.26", default-features = false, features = ["zlib"] } zip = { version = "0.6.6", default-features = false, features = ["time", "deflate-zlib"] } diff --git a/eslint.config.js b/eslint.config.js index 33165a23087..f1b01d02447 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1001,7 +1001,6 @@ export default tseslint.config( { 'target': 'src/vs/workbench/api/~', 'restrictions': [ - '@c4312/eventsource-umd', 'vscode', 'vs/base/~', 'vs/base/parts/*/~', diff --git a/extensions/git/package.json b/extensions/git/package.json index f4ba9efe8b1..3102a464853 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -251,6 +251,13 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.unstageChange", + "title": "%command.unstageChange%", + "category": "Git", + "icon": "$(remove)", + "enablement": "!operationInProgress" + }, { "command": "git.unstageFile", "title": "%command.unstage%", @@ -970,7 +977,7 @@ "command": "git.unstageSelectedRanges", "key": "ctrl+k ctrl+n", "mac": "cmd+k cmd+n", - "when": "editorTextFocus && isInDiffEditor && isInDiffRightEditor && resourceScheme == git" + "when": "editorTextFocus && isInDiffEditor && isInDiffRightEditor && (resourceScheme == file || resourceScheme == git)" }, { "command": "git.revertSelectedRanges", @@ -1075,7 +1082,11 @@ }, { "command": "git.unstageSelectedRanges", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == git" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && (resourceScheme == file || resourceScheme == git)" + }, + { + "command": "git.unstageChange", + "when": "false" }, { "command": "git.clean", @@ -2219,11 +2230,15 @@ "scm/change/title": [ { "command": "git.stageChange", - "when": "config.git.enabled && !git.missing && originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResource =~ /^git\\:.*%22ref%22%3A%22%22%7D$/" }, { "command": "git.revertChange", - "when": "config.git.enabled && !git.missing && originalResourceScheme == git" + "when": "config.git.enabled && !git.missing && originalResource =~ /^git\\:.*%22ref%22%3A%22%22%7D$/" + }, + { + "command": "git.unstageChange", + "when": "config.git.enabled && !git.missing && originalResource =~ /^git\\:.*%22ref%22%3A%22HEAD%22%7D$/" } ], "timeline/item/context": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index dfc2ce99ca8..403f704e2f6 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -28,6 +28,7 @@ "command.revertChange": "Revert Change", "command.unstage": "Unstage Changes", "command.unstageAll": "Unstage All Changes", + "command.unstageChange": "Unstage Change", "command.unstageSelectedRanges": "Unstage Selected Ranges", "command.rename": "Rename", "command.clean": "Discard Changes", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 41456b1245c..610513898d1 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -720,10 +720,13 @@ class GitBlameStatusBarItem { workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); this._controller.onDidChangeBlameInformation(() => this._onDidChangeBlameInformation(), this, this._disposables); + + this._onDidChangeConfiguration(); } - private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.commitShortHashLength') && + private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { + if (e && + !e.affectsConfiguration('git.commitShortHashLength') && !e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a1069359507..6aec2846e63 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1020,18 +1020,37 @@ export class CommandCenter { } } - @command('git.continueInLocalClone') - async continueInLocalClone(): Promise { - if (this.model.repositories.length === 0) { return; } - - // Pick a single repository to continue working on in a local clone if there's more than one - const items = this.model.repositories.reduce<(QuickPickItem & { repository: Repository })[]>((items, repository) => { + private getRepositoriesWithRemote(repositories: Repository[]) { + return repositories.reduce<(QuickPickItem & { repository: Repository })[]>((items, repository) => { const remote = repository.remotes.find((r) => r.name === repository.HEAD?.upstream?.remote); if (remote?.pushUrl) { items.push({ repository: repository, label: remote.pushUrl }); } return items; }, []); + } + + @command('git.continueInLocalClone') + async continueInLocalClone(): Promise { + if (this.model.repositories.length === 0) { return; } + + // Pick a single repository to continue working on in a local clone if there's more than one + let items = this.getRepositoriesWithRemote(this.model.repositories); + + // We have a repository but there is no remote URL (e.g. git init) + if (items.length === 0) { + const pick = this.model.repositories.length === 1 + ? { repository: this.model.repositories[0] } + : await window.showQuickPick(this.model.repositories.map((i) => ({ repository: i, label: i.root })), { canPickMany: false, placeHolder: l10n.t('Choose which repository to publish') }); + if (!pick) { return; } + + await this.publish(pick.repository); + + items = this.getRepositoriesWithRemote([pick.repository]); + if (items.length === 0) { + return; + } + } let selection = items[0]; if (items.length > 1) { @@ -1955,16 +1974,6 @@ export class CommandCenter { const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; - if (!isGitUri(modifiedUri)) { - return; - } - - const { ref } = fromGitUri(modifiedUri); - - if (ref !== '') { - return; - } - const repository = this.model.getRepository(modifiedUri); if (!repository) { return; @@ -1989,6 +1998,7 @@ export class CommandCenter { const originalUri = toGitUri(resource.original, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); const selectedLines = toLineRanges(textEditor.selections, modifiedDocument); + const selectedDiffs = indexLineChanges .map(change => selectedLines.reduce((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null)) .filter(c => !!c) as LineChange[]; @@ -1998,13 +2008,25 @@ export class CommandCenter { return; } - const invertedDiffs = selectedDiffs.map(invertLineChange); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffs: ${JSON.stringify(selectedDiffs)}`); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`); - const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); - await repository.stage(modifiedUri, result, modifiedDocument.encoding); + if (modifiedUri.scheme === 'file') { + // Editor + const changes = indexLineChanges + .filter(c => !selectedDiffs.some(d => + d.originalStartLineNumber === c.originalStartLineNumber && + d.originalEndLineNumber === c.originalEndLineNumber && + d.modifiedStartLineNumber === c.modifiedStartLineNumber && + d.modifiedEndLineNumber === c.modifiedEndLineNumber)); + await this._unstageChanges(textEditor, changes); + return; + } + + const selectedDiffsInverted = selectedDiffs.map(invertLineChange); + this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffsInverted: ${JSON.stringify(selectedDiffsInverted)}`); + + const result = applyLineChanges(modifiedDocument, originalDocument, selectedDiffsInverted); + await repository.stage(modifiedDocument.uri, result, modifiedDocument.encoding); } @command('git.unstageFile') @@ -2029,6 +2051,38 @@ export class CommandCenter { await repository.revert(resources); } + @command('git.unstageChange') + async unstageChange(uri: Uri, changes: LineChange[], index: number): Promise { + if (!uri) { + return; + } + + const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0]; + if (!textEditor) { + return; + } + + changes.splice(index, 1); + await this._unstageChanges(textEditor, changes); + } + + private async _unstageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { + const modifiedDocument = textEditor.document; + const modifiedUri = modifiedDocument.uri; + + if (modifiedUri.scheme !== 'file') { + return; + } + + const originalUri = toGitUri(modifiedUri, 'HEAD'); + const originalDocument = await workspace.openTextDocument(originalUri); + + const invertedChanges = changes.map(invertLineChange); + const result = applyLineChanges(originalDocument, modifiedDocument, invertedChanges); + + await this.runByRepository(modifiedUri, async (repository, resource) => + await repository.stage(resource, result, modifiedDocument.encoding)); + } @command('git.clean') async clean(...resourceStates: SourceControlResourceState[]): Promise { diff --git a/extensions/git/src/editSessionIdentityProvider.ts b/extensions/git/src/editSessionIdentityProvider.ts index 6a0a31774a1..8e172f2d174 100644 --- a/extensions/git/src/editSessionIdentityProvider.ts +++ b/extensions/git/src/editSessionIdentityProvider.ts @@ -13,11 +13,12 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit private providerRegistration: vscode.Disposable; constructor(private model: Model) { - this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this); - - vscode.workspace.onWillCreateEditSessionIdentity((e) => { - e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder)); - }); + this.providerRegistration = vscode.Disposable.from( + vscode.workspace.registerEditSessionIdentityProvider('file', this), + vscode.workspace.onWillCreateEditSessionIdentity((e) => { + e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder)); + }) + ); } dispose() { @@ -81,9 +82,23 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit await repository.status(); - // If this branch hasn't been published to the remote yet, - // ensure that it is published before Continue On is invoked - if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) { + if (!repository.HEAD?.commit) { + // Handle publishing empty repository with no commits + + const yes = vscode.l10n.t('Yes'); + const selection = await vscode.window.showInformationMessage( + vscode.l10n.t('Would you like to publish this repository to continue working on it elsewhere?'), + { modal: true }, + yes + ); + if (selection !== yes) { + throw new vscode.CancellationError(); + } + await repository.commit('Initial commit', { all: true }); + await vscode.commands.executeCommand('git.publish'); + } else if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) { + // If this branch hasn't been published to the remote yet, + // ensure that it is published before Continue On is invoked const publishBranch = vscode.l10n.t('Publish Branch'); const selection = await vscode.window.showInformationMessage( diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index e81b4ce508f..a55c103c9ef 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -27,7 +27,6 @@ import { GitPostCommitCommandsProvider } from './postCommitCommands'; import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider'; import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics'; import { GitBlameController } from './blame'; -import { StagedResourceQuickDiffProvider } from './repository'; const deactivateTasks: { (): Promise }[] = []; @@ -117,7 +116,6 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, new GitBlameController(model), new GitTimelineProvider(model, cc), new GitEditSessionIdentityProvider(model), - new StagedResourceQuickDiffProvider(model, logger), new TerminalShellExecutionManager(model, logger) ); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c36cbe30f11..c2b24e460ea 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -68,15 +68,13 @@ export class Resource implements SourceControlResourceState { return 'U'; case Status.IGNORED: return 'I'; - case Status.DELETED_BY_THEM: - return 'D'; - case Status.DELETED_BY_US: - return 'D'; case Status.INDEX_COPIED: return 'C'; case Status.BOTH_DELETED: case Status.ADDED_BY_US: + case Status.DELETED_BY_THEM: case Status.ADDED_BY_THEM: + case Status.DELETED_BY_US: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows @@ -892,6 +890,7 @@ export class Repository implements Disposable { this._sourceControl = scm.createSourceControl('git', 'Git', root); this._sourceControl.quickDiffProvider = this; + this._sourceControl.secondaryQuickDiffProvider = new StagedResourceQuickDiffProvider(this, logger); this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); this._sourceControl.historyProvider = this._historyProvider; @@ -1240,6 +1239,9 @@ export class Repository implements Disposable { Operation.RevertFiles(!this.optimisticUpdateEnabled()), async () => { await this.repository.revert('HEAD', resources.map(r => r.fsPath)); + for (const resource of resources) { + this._onDidChangeOriginalResource.fire(resource); + } this.closeDiffEditors([...resources.length !== 0 ? resources.map(r => r.fsPath) : this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)], []); @@ -2803,29 +2805,19 @@ export class Repository implements Disposable { } export class StagedResourceQuickDiffProvider implements QuickDiffProvider { - readonly visible: boolean = false; - - private _disposables: IDisposable[] = []; + readonly visible: boolean = true; + readonly label = l10n.t('Git local changes (index)'); constructor( - private readonly _repositoryResolver: IRepositoryResolver, + private readonly _repository: Repository, private readonly logger: LogOutputChannel - ) { - this._disposables.push(window.registerQuickDiffProvider({ scheme: 'file' }, this, l10n.t('Git local changes (working tree + index)'))); - } + ) { } provideOriginalResource(uri: Uri): Uri | undefined { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource: ${uri.toString()}`); - // Ignore resources outside a repository - const repository = this._repositoryResolver.getRepository(uri); - if (!repository) { - this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of the repository: ${uri.toString()}`); - return undefined; - } - // Ignore resources that are not in the index group - if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); return undefined; } @@ -2834,8 +2826,4 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Original resource: ${originalResource.toString()}`); return originalResource; } - - dispose() { - this._disposables = dispose(this._disposables); - } } diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index ec7232bec44..208f1e99f57 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -186,10 +186,9 @@ export function toLineChanges(diffInformation: TextEditorDiffInformation): LineC } export function getIndexDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { - // Diff Editor (Index) + // Diff Editor (Index) | Text Editor return textEditor.diffInformation?.find(diff => - diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD' && - diff.modified && isGitUri(diff.modified) && fromGitUri(diff.modified).ref === ''); + diff.original && isGitUri(diff.original) && fromGitUri(diff.original).ref === 'HEAD'); } export function getWorkingTreeDiffInformation(textEditor: TextEditor): TextEditorDiffInformation | undefined { diff --git a/extensions/github/package-lock.json b/extensions/github/package-lock.json index cf7317a40a4..1b7dc727a92 100644 --- a/extensions/github/package-lock.json +++ b/extensions/github/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@octokit/graphql": "8.2.0", + "@octokit/graphql": "5.0.5", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "21.1.0", + "@octokit/rest": "19.0.4", "@vscode/extension-telemetry": "^0.9.8", "tunnel": "^0.0.6" }, @@ -147,57 +147,96 @@ "license": "MIT" }, "node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", - "license": "MIT", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.1.tgz", + "integrity": "sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==", + "dependencies": { + "@octokit/types": "^7.0.0" + }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/auth-token/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/auth-token/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/core": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz", - "integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==", - "license": "MIT", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.5.tgz", + "integrity": "sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==", "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.1.2", - "@octokit/request": "^9.2.1", - "@octokit/request-error": "^6.1.7", - "@octokit/types": "^13.6.2", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^7.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/endpoint": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", - "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", - "license": "MIT", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.1.tgz", + "integrity": "sha512-/wTXAJwt0HzJ2IeE4kQXO+mBScfzyCkI0hMtkIaqyXd9zg76OpOfNQfHL9FlaxAV2RsNiOXZibVWloy8EexENg==", "dependencies": { - "@octokit/types": "^13.6.2", - "universal-user-agent": "^7.0.2" + "@octokit/types": "^7.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/graphql": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.0.tgz", - "integrity": "sha512-gejfDywEml/45SqbWTWrhfwvLBrcGYhOn50sPOjIeVvH6i7D16/9xcFA8dAJNp2HMcd+g4vru41g4E2RBiZvfQ==", - "license": "MIT", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", + "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", "dependencies": { - "@octokit/request": "^9.1.4", - "@octokit/types": "^13.8.0", - "universal-user-agent": "^7.0.0" + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/graphql-schema": { @@ -210,103 +249,148 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-17.1.0.tgz", + "integrity": "sha512-rnI26BAITDZTo5vqFOmA7oX4xRd18rO+gcK4MiTpJmsRMxAw0JmevNjPsjpry1bb9SVNo56P/0kbiyXXa4QluA==" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.4.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.2.tgz", - "integrity": "sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==", - "license": "MIT", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.0.tgz", + "integrity": "sha512-8otLCIK9esfmOCY14CBnG/xPqv0paf14rc+s9tHpbOpeFwrv5CnECKW1qdqMAT60ngAa9eB1bKQ+l2YCpi0HPQ==", "dependencies": { - "@octokit/types": "^13.7.0" + "@octokit/types": "^7.2.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/plugin-request-log": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz", - "integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==", - "license": "MIT", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.4.0.tgz", + "integrity": "sha512-YP4eUqZ6vORy/eZOTdil1ZSrMt0kv7i/CVw+HhC2C0yJN+IqTc/rot957JQ7JfyeJD6HZOjLg6Jp1o9cPhI9KA==", "dependencies": { - "@octokit/types": "^13.8.0" + "@octokit/types": "^7.2.0", + "deprecation": "^2.3.1" }, "engines": { - "node": ">= 18" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/request": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.1.tgz", - "integrity": "sha512-TqHLIdw1KFvx8WvLc7Jv94r3C3+AzKY2FWq7c20zvrxmCIa6MCVkLCE/826NCXnml3LFJjLsidDh1BhMaGEDQw==", - "license": "MIT", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz", + "integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==", "dependencies": { - "@octokit/endpoint": "^10.1.3", - "@octokit/request-error": "^6.1.6", - "@octokit/types": "^13.6.2", - "fast-content-type-parse": "^2.0.0", - "universal-user-agent": "^7.0.2" + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^7.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/request-error": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", - "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", - "license": "MIT", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz", + "integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==", "dependencies": { - "@octokit/types": "^13.6.2" + "@octokit/types": "^7.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.6.0.tgz", + "integrity": "sha512-bxftLwoZ2J6zsU1rzRvk0O32j7lVB0NWWn+P5CDHn9zPzytasR3hdAeXlTngRDkqv1LyEeuy5psVnDkmOSwrcQ==" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.2.0.tgz", + "integrity": "sha512-pYQ/a1U6mHptwhGyp6SvsiM4bWP2s3V95olUeTxas85D/2kN78yN5C8cGN+P4LwJSWUqIEyvq0Qn2WUn6NQRjw==", + "dependencies": { + "@octokit/openapi-types": "^13.6.0" } }, "node_modules/@octokit/rest": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.0.tgz", - "integrity": "sha512-93iLxcKDJboUpmnUyeJ6cRIi7z7cqTZT1K7kRK4LobGxwTwpsa+2tQQbRQNGy7IFDEAmrtkf4F4wBj3D5rVlJQ==", - "license": "MIT", + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.4.tgz", + "integrity": "sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==", "dependencies": { - "@octokit/core": "^6.1.3", - "@octokit/plugin-paginate-rest": "^11.4.0", - "@octokit/plugin-request-log": "^5.3.1", - "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + "@octokit/core": "^4.0.0", + "@octokit/plugin-paginate-rest": "^4.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 14" } }, "node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.2.0.tgz", + "integrity": "sha512-xySzJG4noWrIBFyMu4lg4tu9vAgNg9S0aoLRONhAEz6ueyi1evBzb40HitIosaYS4XOexphG305IVcLrIX/30g==", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^17.1.0" } }, "node_modules/@types/node": { @@ -333,26 +417,14 @@ } }, "node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "license": "Apache-2.0" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" }, - "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, "node_modules/graphql": { "version": "16.8.1", @@ -376,6 +448,46 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -396,10 +508,28 @@ "dev": true }, "node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "license": "ISC" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" } } } diff --git a/extensions/github/package.json b/extensions/github/package.json index 86adc2ddc4e..524cee5bbea 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -227,9 +227,9 @@ "watch": "gulp watch-extension:github" }, "dependencies": { - "@octokit/graphql": "8.2.0", + "@octokit/graphql": "5.0.5", "@octokit/graphql-schema": "14.4.0", - "@octokit/rest": "21.1.0", + "@octokit/rest": "19.0.4", "tunnel": "^0.0.6", "@vscode/extension-telemetry": "^0.9.8" }, diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 15054d499e7..44d13c211f1 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -371,7 +371,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP // handle content request client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { const uri = Uri.parse(uriPath); - const uriString = uri.toString(); + const uriString = uri.toString(true); if (uri.scheme === 'untitled') { throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); } diff --git a/extensions/php-language-features/src/features/phpGlobalFunctions.ts b/extensions/php-language-features/src/features/phpGlobalFunctions.ts index ab1c5487ae8..e8f10a29db5 100644 --- a/extensions/php-language-features/src/features/phpGlobalFunctions.ts +++ b/extensions/php-language-features/src/features/phpGlobalFunctions.ts @@ -1664,31 +1664,35 @@ export const globalfunctions: IEntries = { }, fclose: { description: 'Closes an open file pointer', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' + }, + fdatasync: { + description: 'Synchronizes data (but not meta-data) to the file', + signature: '( resource $stream ): bool' }, feof: { description: 'Tests for end-of-file on a file pointer', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' }, fflush: { description: 'Flushes the output to a file', - signature: '( resource $handle ): bool' + signature: '( resource $stream ): bool' }, fgetc: { description: 'Gets character from file pointer', - signature: '( resource $handle ): string' + signature: '( resource $string ): string|false' }, fgetcsv: { description: 'Gets line from file pointer and parse for CSV fields', - signature: '( resource $handle [, int $length = 0 [, string $delimiter = "," [, string $enclosure = \'"\' [, string $escape = "\\" ]]]]): array' + signature: '( resource $stream [, ?int $length = null [, string $separator = "," [, string $enclosure = \'"\' [, string $escape = "\\" ]]]]): array|false' }, fgets: { description: 'Gets line from file pointer', - signature: '( resource $handle [, int $length ]): string' + signature: '( resource $stream [, ?int $length = null ]): string|false' }, fgetss: { description: 'Gets line from file pointer and strip HTML tags', - signature: '( resource $handle [, int $length [, string $allowable_tags ]]): string' + signature: '( resource $handle [, int $length = ? [, string $allowable_tags = ? ]]): string' }, file_exists: { description: 'Checks whether a file or directory exists', @@ -1696,102 +1700,106 @@ export const globalfunctions: IEntries = { }, file_get_contents: { description: 'Reads entire file into a string', - signature: '( string $filename [, bool $use_include_path [, resource $context [, int $offset = 0 [, int $maxlen ]]]]): string' + signature: '( string $filename [, bool $use_include_path = false [, ?resource $context = null [, int $offset = 0 [, ?int $maxlen = null ]]]]): string|false' }, file_put_contents: { description: 'Write data to a file', - signature: '( string $filename , mixed $data [, int $flags = 0 [, resource $context ]]): int' + signature: '( string $filename , mixed $data [, int $flags = 0 [, ?resource $context = null ]]): int|false' }, file: { description: 'Reads entire file into an array', - signature: '( string $filename [, int $flags = 0 [, resource $context ]]): array' + signature: '( string $filename [, int $flags = 0 [, ?resource $context = null ]]): array|false' }, fileatime: { description: 'Gets last access time of file', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filectime: { description: 'Gets inode change time of file', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filegroup: { description: 'Gets file group', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileinode: { description: 'Gets file inode', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filemtime: { description: 'Gets file modification time', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileowner: { description: 'Gets file owner', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, fileperms: { description: 'Gets file permissions', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filesize: { description: 'Gets file size', - signature: '( string $filename ): int' + signature: '( string $filename ): int|false' }, filetype: { description: 'Gets file type', - signature: '( string $filename ): string' + signature: '( string $filename ): string|false' }, flock: { description: 'Portable advisory file locking', - signature: '( resource $handle , int $operation [, int $wouldblock ]): bool' + signature: '( resource $stream , int $operation [, int &$would_block = null ]): bool' }, fnmatch: { description: 'Match filename against a pattern', - signature: '( string $pattern , string $string [, int $flags = 0 ]): bool' + signature: '( string $pattern , string $filename [, int $flags = 0 ]): bool' }, fopen: { description: 'Opens file or URL', - signature: '( string $filename , string $mode [, bool $use_include_path [, resource $context ]]): resource' + signature: '( string $filename , string $mode [, bool $use_include_path = false [, ?resource $context = null ]]): resource|false' }, fpassthru: { description: 'Output all remaining data on a file pointer', - signature: '( resource $handle ): int' + signature: '( resource $stream ): int' }, fputcsv: { description: 'Format line as CSV and write to file pointer', - signature: '( resource $handle , array $fields [, string $delimiter = "," [, string $enclosure = \'"\' [, string $escape_char = "\\" ]]]): int' + signature: '( resource $stream , array $fields [, string $separator = "," [, string $enclosure = \'"\' [, string $escape = "\\" [, string $eol = "\n" ]]]]): int|false' }, fputs: { description: 'Alias of fwrite', }, fread: { description: 'Binary-safe file read', - signature: '( resource $handle , int $length ): string' + signature: '( resource $stream , int $length ): string|false' }, fscanf: { description: 'Parses input from a file according to a format', - signature: '( resource $handle , string $format [, mixed $... ]): mixed' + signature: '( resource $stream , string $format [, mixed &...$vars ]): array|int|false|null' }, fseek: { description: 'Seeks on a file pointer', - signature: '( resource $handle , int $offset [, int $whence = SEEK_SET ]): int' + signature: '( resource $stream , int $offset [, int $whence = SEEK_SET ]): int' }, fstat: { description: 'Gets information about a file using an open file pointer', - signature: '( resource $handle ): array' + signature: '( resource $stream ): array|false' + }, + fsync: { + description: 'Synchronizes changes to the file (including meta-data)', + signature: '( resource $stream ): bool' }, ftell: { description: 'Returns the current position of the file read/write pointer', - signature: '( resource $handle ): int' + signature: '( resource $stream ): int|false' }, ftruncate: { description: 'Truncates a file to a given length', - signature: '( resource $handle , int $size ): bool' + signature: '( resource $stream , int $size ): bool' }, fwrite: { description: 'Binary-safe file write', - signature: '( resource $handle , string $string [, int $length ]): int' + signature: '( resource $stream , string $data [, ?int $length = null ]): int|false' }, glob: { description: 'Find pathnames matching a pattern', diff --git a/extensions/prompt-basics/package.json b/extensions/prompt-basics/package.json index 96fa45e18f7..9d56f9f92f0 100644 --- a/extensions/prompt-basics/package.json +++ b/extensions/prompt-basics/package.json @@ -22,6 +22,18 @@ "copilot-instructions.md" ], "configuration": "./language-configuration.json" + }, + { + "id": "instructions", + "aliases": [ + "Instructions", + "instructions" + ], + "extensions": [ + ".instructions.md", + "copilot-instructions.md" + ], + "configuration": "./language-configuration.json" } ], "grammars": [ @@ -33,6 +45,15 @@ "markup.underline.link.markdown", "punctuation.definition.list.begin.markdown" ] + }, + { + "language": "instructions", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] } ], "configurationDefaults": { @@ -40,6 +61,11 @@ "editor.unicodeHighlight.ambiguousCharacters": false, "editor.unicodeHighlight.invisibleCharacters": false, "diffEditor.ignoreTrimWhitespace": false + }, + "[instructions]": { + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false } } }, diff --git a/extensions/prompt-basics/package.nls.json b/extensions/prompt-basics/package.nls.json index 1a98e5f9ca4..207593c43c8 100644 --- a/extensions/prompt-basics/package.nls.json +++ b/extensions/prompt-basics/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Prompt Language Basics", - "description": "Syntax highlighting for Prompt documents." + "description": "Syntax highlighting for Prompt and Instructions documents." } diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 18efa329a0f..3e923dde3a9 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -34,7 +34,8 @@ export const enum TerminalShellType { Fish = 'fish', Zsh = 'zsh', PowerShell = 'pwsh', - Python = 'python' + Python = 'python', + GitBash = 'gitbash', } const isWindows = osIsWindows(); @@ -322,6 +323,8 @@ function getTerminalShellType(shellType: string | undefined): TerminalShellType switch (shellType) { case 'bash': return TerminalShellType.Bash; + case 'gitbash': + return TerminalShellType.GitBash; case 'zsh': return TerminalShellType.Zsh; case 'pwsh': diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 541313a81cf..072cca645ff 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -147,1370 +147,1420 @@ "url": "https://typedoc.org/schema.json" } ], - "configuration": { - "type": "object", - "title": "%configuration.typescript%", - "order": 20, - "properties": { - "typescript.tsdk": { - "type": "string", - "markdownDescription": "%typescript.tsdk.desc%", - "scope": "window" - }, - "typescript.disableAutomaticTypeAcquisition": { - "type": "boolean", - "default": false, - "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", - "scope": "window", - "tags": [ - "usesOnlineServices" - ] - }, - "typescript.enablePromptUseWorkspaceTsdk": { - "type": "boolean", - "default": false, - "description": "%typescript.enablePromptUseWorkspaceTsdk%", - "scope": "window" - }, - "typescript.npm": { - "type": "string", - "markdownDescription": "%typescript.npm%", - "scope": "machine" - }, - "typescript.check.npmIsInstalled": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.check.npmIsInstalled%", - "scope": "window" - }, - "javascript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%javascript.referencesCodeLens.enabled%", - "scope": "window" - }, - "javascript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "description": "%javascript.referencesCodeLens.showOnAllFunctions%", - "scope": "window" - }, - "typescript.referencesCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%typescript.referencesCodeLens.enabled%", - "scope": "window" - }, - "typescript.referencesCodeLens.showOnAllFunctions": { - "type": "boolean", - "default": false, - "description": "%typescript.referencesCodeLens.showOnAllFunctions%", - "scope": "window" - }, - "typescript.implementationsCodeLens.enabled": { - "type": "boolean", - "default": false, - "description": "%typescript.implementationsCodeLens.enabled%", - "scope": "window" - }, - "typescript.implementationsCodeLens.showOnInterfaceMethods": { - "type": "boolean", - "default": false, - "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", - "scope": "window" - }, - "typescript.tsserver.enableTracing": { - "type": "boolean", - "default": false, - "description": "%typescript.tsserver.enableTracing%", - "scope": "window" - }, - "typescript.tsserver.log": { - "type": "string", - "enum": [ - "off", - "terse", - "normal", - "verbose" - ], - "default": "off", - "description": "%typescript.tsserver.log%", - "scope": "window" - }, - "typescript.tsserver.pluginPaths": { - "type": "array", - "items": { + "configuration": [ + { + "type": "object", + "order": 20, + "properties": { + "typescript.tsdk": { "type": "string", - "description": "%typescript.tsserver.pluginPaths.item%" + "markdownDescription": "%typescript.tsdk.desc%", + "scope": "window" }, - "default": [], - "description": "%typescript.tsserver.pluginPaths%", - "scope": "machine" - }, - "javascript.suggest.completeFunctionCalls": { - "type": "boolean", - "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "scope": "resource" - }, - "typescript.suggest.completeFunctionCalls": { - "type": "boolean", - "default": false, - "description": "%configuration.suggest.completeFunctionCalls%", - "scope": "resource" - }, - "javascript.suggest.includeAutomaticOptionalChainCompletions": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "scope": "resource" - }, - "typescript.suggest.includeAutomaticOptionalChainCompletions": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.enabled": { - "type": "string", - "enum": [ - "none", - "literals", - "all" - ], - "enumDescriptions": [ - "%inlayHints.parameterNames.none%", - "%inlayHints.parameterNames.literals%", - "%inlayHints.parameterNames.all%" - ], - "default": "none", - "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.enumMemberValues.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.parameterNames.enabled": { - "type": "string", - "enum": [ - "none", - "literals", - "all" - ], - "enumDescriptions": [ - "%inlayHints.parameterNames.none%", - "%inlayHints.parameterNames.literals%", - "%inlayHints.parameterNames.all%" - ], - "default": "none", - "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "javascript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "javascript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "javascript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "javascript.suggest.includeCompletionsForImportStatements": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "scope": "resource" - }, - "typescript.suggest.includeCompletionsForImportStatements": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.includeCompletionsForImportStatements%", - "scope": "resource" - }, - "typescript.reportStyleChecksAsWarnings": { - "type": "boolean", - "default": true, - "description": "%typescript.reportStyleChecksAsWarnings%", - "scope": "window" - }, - "typescript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%typescript.validate.enable%", - "scope": "window" - }, - "typescript.format.enable": { - "type": "boolean", - "default": true, - "description": "%typescript.format.enable%", - "scope": "window" - }, - "typescript.format.insertSpaceAfterCommaDelimiter": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterConstructor": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterSemicolonInForStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "scope": "resource" - }, - "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "scope": "resource" - }, - "typescript.format.insertSpaceBeforeFunctionParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "scope": "resource" - }, - "typescript.format.insertSpaceAfterTypeAssertion": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterTypeAssertion%", - "scope": "resource" - }, - "typescript.format.placeOpenBraceOnNewLineForFunctions": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "scope": "resource" - }, - "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "scope": "resource" - }, - "typescript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] - }, - "typescript.format.indentSwitchCase": { - "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "scope": "resource" - }, - "javascript.format.indentSwitchCase": { - "type": "boolean", - "default": true, - "description": "%format.indentSwitchCase%", - "scope": "resource" - }, - "javascript.validate.enable": { - "type": "boolean", - "default": true, - "description": "%javascript.validate.enable%", - "scope": "window" - }, - "javascript.format.enable": { - "type": "boolean", - "default": true, - "description": "%javascript.format.enable%", - "scope": "window" - }, - "javascript.format.insertSpaceAfterCommaDelimiter": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterCommaDelimiter%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterConstructor": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterConstructor%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterSemicolonInForStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterSemicolonInForStatements%", - "scope": "resource" - }, - "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", - "scope": "resource" - }, - "javascript.format.insertSpaceBeforeFunctionParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceBeforeFunctionParenthesis%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { - "type": "boolean", - "default": true, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", - "scope": "resource" - }, - "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { - "type": "boolean", - "default": false, - "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", - "scope": "resource" - }, - "javascript.format.placeOpenBraceOnNewLineForFunctions": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForFunctions%", - "scope": "resource" - }, - "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { - "type": "boolean", - "default": false, - "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", - "scope": "resource" - }, - "javascript.format.semicolons": { - "type": "string", - "default": "ignore", - "description": "%format.semicolons%", - "scope": "resource", - "enum": [ - "ignore", - "insert", - "remove" - ], - "enumDescriptions": [ - "%format.semicolons.ignore%", - "%format.semicolons.insert%", - "%format.semicolons.remove%" - ] - }, - "js/ts.implicitProjectConfig.module": { - "type": "string", - "markdownDescription": "%configuration.implicitProjectConfig.module%", - "default": "ESNext", - "enum": [ - "CommonJS", - "AMD", - "System", - "UMD", - "ES6", - "ES2015", - "ES2020", - "ESNext", - "None", - "ES2022", - "Node12", - "NodeNext" - ], - "scope": "window" - }, - "js/ts.implicitProjectConfig.target": { - "type": "string", - "default": "ES2022", - "markdownDescription": "%configuration.implicitProjectConfig.target%", - "enum": [ - "ES3", - "ES5", - "ES6", - "ES2015", - "ES2016", - "ES2017", - "ES2018", - "ES2019", - "ES2020", - "ES2021", - "ES2022", - "ES2023", - "ES2024", - "ESNext" - ], - "scope": "window" - }, - "javascript.implicitProjectConfig.checkJs": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", - "markdownDeprecationMessage": "%configuration.javascript.checkJs.checkJs.deprecation%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.checkJs": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", - "scope": "window" - }, - "javascript.implicitProjectConfig.experimentalDecorators": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", - "markdownDeprecationMessage": "%configuration.javascript.checkJs.experimentalDecorators.deprecation%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.experimentalDecorators": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictNullChecks": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", - "scope": "window" - }, - "js/ts.implicitProjectConfig.strictFunctionTypes": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", - "scope": "window" - }, - "javascript.suggest.names": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.names%", - "scope": "resource" - }, - "typescript.tsc.autoDetect": { - "type": "string", - "default": "on", - "enum": [ - "on", - "off", - "build", - "watch" - ], - "markdownEnumDescriptions": [ - "%typescript.tsc.autoDetect.on%", - "%typescript.tsc.autoDetect.off%", - "%typescript.tsc.autoDetect.build%", - "%typescript.tsc.autoDetect.watch%" - ], - "description": "%typescript.tsc.autoDetect%", - "scope": "window" - }, - "javascript.suggest.paths": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.paths%", - "scope": "resource" - }, - "typescript.suggest.paths": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.paths%", - "scope": "resource" - }, - "javascript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "scope": "resource" - }, - "typescript.suggest.autoImports": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.autoImports%", - "scope": "resource" - }, - "javascript.suggest.completeJSDocs": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "scope": "language-overridable" - }, - "typescript.suggest.completeJSDocs": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.completeJSDocs%", - "scope": "language-overridable" - }, - "javascript.suggest.jsdoc.generateReturns": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "scope": "language-overridable" - }, - "typescript.suggest.jsdoc.generateReturns": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", - "scope": "language-overridable" - }, - "typescript.locale": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "de", - "es", - "en", - "fr", - "it", - "ja", - "ko", - "ru", - "zh-CN", - "zh-TW" - ], - "enumDescriptions": [ - "%typescript.locale.auto%", - "Deutsch", - "español", - "English", - "français", - "italiano", - "日本語", - "한국어", - "русский", - "中文(简体)", - "中文(繁體)" - ], - "markdownDescription": "%typescript.locale%", - "scope": "window" - }, - "javascript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%javascript.suggestionActions.enabled%", - "scope": "resource" - }, - "typescript.suggestionActions.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggestionActions.enabled%", - "scope": "resource" - }, - "javascript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "scope": "language-overridable" - }, - "typescript.preferences.quoteStyle": { - "type": "string", - "enum": [ - "auto", - "single", - "double" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.quoteStyle%", - "markdownEnumDescriptions": [ - "%typescript.preferences.quoteStyle.auto%", - "%typescript.preferences.quoteStyle.single%", - "%typescript.preferences.quoteStyle.double%" - ], - "scope": "language-overridable" - }, - "javascript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "scope": "language-overridable" - }, - "typescript.preferences.importModuleSpecifier": { - "type": "string", - "enum": [ - "shortest", - "relative", - "non-relative", - "project-relative" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifier.shortest%", - "%typescript.preferences.importModuleSpecifier.relative%", - "%typescript.preferences.importModuleSpecifier.nonRelative%", - "%typescript.preferences.importModuleSpecifier.projectRelative%" - ], - "default": "shortest", - "description": "%typescript.preferences.importModuleSpecifier%", - "scope": "language-overridable" - }, - "javascript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "scope": "language-overridable" - }, - "typescript.preferences.importModuleSpecifierEnding": { - "type": "string", - "enum": [ - "auto", - "minimal", - "index", - "js" - ], - "enumItemLabels": [ - null, - null, - null, - "%typescript.preferences.importModuleSpecifierEnding.label.js%" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.importModuleSpecifierEnding.auto%", - "%typescript.preferences.importModuleSpecifierEnding.minimal%", - "%typescript.preferences.importModuleSpecifierEnding.index%", - "%typescript.preferences.importModuleSpecifierEnding.js%" - ], - "default": "auto", - "description": "%typescript.preferences.importModuleSpecifierEnding%", - "scope": "language-overridable" - }, - "javascript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%javascript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "scope": "language-overridable" - }, - "typescript.preferences.jsxAttributeCompletionStyle": { - "type": "string", - "enum": [ - "auto", - "braces", - "none" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.jsxAttributeCompletionStyle.auto%", - "%typescript.preferences.jsxAttributeCompletionStyle.braces%", - "%typescript.preferences.jsxAttributeCompletionStyle.none%" - ], - "default": "auto", - "description": "%typescript.preferences.jsxAttributeCompletionStyle%", - "scope": "language-overridable" - }, - "typescript.preferences.includePackageJsonAutoImports": { - "type": "string", - "enum": [ - "auto", - "on", - "off" - ], - "enumDescriptions": [ - "%typescript.preferences.includePackageJsonAutoImports.auto%", - "%typescript.preferences.includePackageJsonAutoImports.on%", - "%typescript.preferences.includePackageJsonAutoImports.off%" - ], - "default": "auto", - "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", - "scope": "window" - }, - "typescript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" + "typescript.disableAutomaticTypeAcquisition": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.disableAutomaticTypeAcquisition%", + "scope": "window", + "tags": [ + "usesOnlineServices" + ] }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "scope": "resource" - }, - "javascript.preferences.autoImportFileExcludePatterns": { - "type": "array", - "items": { - "type": "string" + "typescript.enablePromptUseWorkspaceTsdk": { + "type": "boolean", + "default": false, + "description": "%typescript.enablePromptUseWorkspaceTsdk%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", - "scope": "resource" - }, - "typescript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" + "javascript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%javascript.referencesCodeLens.enabled%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "scope": "resource" - }, - "javascript.preferences.autoImportSpecifierExcludeRegexes": { - "type": "array", - "items": { - "type": "string" + "javascript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "%javascript.referencesCodeLens.showOnAllFunctions%", + "scope": "window" }, - "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", - "scope": "resource" - }, - "typescript.preferences.preferTypeOnlyAutoImports": { - "type": "boolean", - "default": false, - "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", - "scope": "resource" - }, - "javascript.preferences.renameShorthandProperties": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", - "scope": "language-overridable" - }, - "typescript.preferences.renameShorthandProperties": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", - "scope": "language-overridable" - }, - "javascript.preferences.useAliasesForRenames": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "scope": "language-overridable" - }, - "typescript.preferences.useAliasesForRenames": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.useAliasesForRenames%", - "scope": "language-overridable" - }, - "javascript.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "scope": "language-overridable" - }, - "typescript.preferences.renameMatchingJsxTags": { - "type": "boolean", - "default": true, - "description": "%typescript.preferences.renameMatchingJsxTags%", - "scope": "language-overridable" - }, - "typescript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } + "typescript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.enabled%", + "scope": "window" + }, + "typescript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.showOnAllFunctions%", + "scope": "window" + }, + "typescript.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.enabled%", + "scope": "window" + }, + "typescript.implementationsCodeLens.showOnInterfaceMethods": { + "type": "boolean", + "default": false, + "description": "%typescript.implementationsCodeLens.showOnInterfaceMethods%", + "scope": "window" + }, + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "resource" + }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "resource" + }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "resource" + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "resource" + }, + "javascript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "resource" + }, + "javascript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "resource" + }, + "javascript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "resource" + }, + "typescript.reportStyleChecksAsWarnings": { + "type": "boolean", + "default": true, + "description": "%typescript.reportStyleChecksAsWarnings%", + "scope": "window" + }, + "typescript.validate.enable": { + "type": "boolean", + "default": true, + "description": "%typescript.validate.enable%", + "scope": "window" + }, + "javascript.validate.enable": { + "type": "boolean", + "default": true, + "description": "%javascript.validate.enable%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.module": { + "type": "string", + "markdownDescription": "%configuration.implicitProjectConfig.module%", + "default": "ESNext", + "enum": [ + "CommonJS", + "AMD", + "System", + "UMD", + "ES6", + "ES2015", + "ES2020", + "ESNext", + "None", + "ES2022", + "Node12", + "NodeNext" + ], + "scope": "window" + }, + "js/ts.implicitProjectConfig.target": { + "type": "string", + "default": "ES2022", + "markdownDescription": "%configuration.implicitProjectConfig.target%", + "enum": [ + "ES3", + "ES5", + "ES6", + "ES2015", + "ES2016", + "ES2017", + "ES2018", + "ES2019", + "ES2020", + "ES2021", + "ES2022", + "ES2023", + "ES2024", + "ESNext" + ], + "scope": "window" + }, + "javascript.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", + "markdownDeprecationMessage": "%configuration.javascript.checkJs.checkJs.deprecation%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.checkJs": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", + "scope": "window" + }, + "javascript.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", + "markdownDeprecationMessage": "%configuration.javascript.checkJs.experimentalDecorators.deprecation%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.experimentalDecorators": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.implicitProjectConfig.experimentalDecorators%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictNullChecks": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strictNullChecks%", + "scope": "window" + }, + "js/ts.implicitProjectConfig.strictFunctionTypes": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.implicitProjectConfig.strictFunctionTypes%", + "scope": "window" + }, + "typescript.tsc.autoDetect": { + "type": "string", + "default": "on", + "enum": [ + "on", + "off", + "build", + "watch" + ], + "markdownEnumDescriptions": [ + "%typescript.tsc.autoDetect.on%", + "%typescript.tsc.autoDetect.off%", + "%typescript.tsc.autoDetect.build%", + "%typescript.tsc.autoDetect.watch%" + ], + "description": "%typescript.tsc.autoDetect%", + "scope": "window" + }, + "typescript.locale": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "de", + "es", + "en", + "fr", + "it", + "ja", + "ko", + "ru", + "zh-CN", + "zh-TW" + ], + "enumDescriptions": [ + "%typescript.locale.auto%", + "Deutsch", + "español", + "English", + "français", + "italiano", + "日本語", + "한국어", + "русский", + "中文(简体)", + "中文(繁體)" + ], + "markdownDescription": "%typescript.locale%", + "scope": "window" + }, + "javascript.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%javascript.suggestionActions.enabled%", + "scope": "resource" + }, + "typescript.suggestionActions.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggestionActions.enabled%", + "scope": "resource" + }, + "typescript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" + }, + "javascript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "markdownEnumDescriptions": [ + "%typescript.updateImportsOnFileMove.enabled.prompt%", + "%typescript.updateImportsOnFileMove.enabled.always%", + "%typescript.updateImportsOnFileMove.enabled.never%" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" + }, + "typescript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%", + "scope": "language-overridable" + }, + "javascript.autoClosingTags": { + "type": "boolean", + "default": true, + "description": "%typescript.autoClosingTags%", + "scope": "language-overridable" + }, + "typescript.workspaceSymbols.scope": { + "type": "string", + "enum": [ + "allOpenProjects", + "currentProject" + ], + "enumDescriptions": [ + "%typescript.workspaceSymbols.scope.allOpenProjects%", + "%typescript.workspaceSymbols.scope.currentProject%" + ], + "default": "allOpenProjects", + "markdownDescription": "%typescript.workspaceSymbols.scope%", + "scope": "window" + }, + "typescript.preferGoToSourceDefinition": { + "type": "boolean", + "default": false, + "description": "%configuration.preferGoToSourceDefinition%", + "scope": "window" + }, + "javascript.preferGoToSourceDefinition": { + "type": "boolean", + "default": false, + "description": "%configuration.preferGoToSourceDefinition%", + "scope": "window" + }, + "typescript.workspaceSymbols.excludeLibrarySymbols": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "scope": "window" + }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, + "javascript.updateImportsOnPaste.enabled": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" + }, + "typescript.updateImportsOnPaste.enabled": { + "scope": "window", + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.updateImportsOnPaste%" } - }, - "javascript.preferences.organizeImports": { - "type": "object", - "markdownDescription": "%typescript.preferences.organizeImports%", - "properties": { - "caseSensitivity": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", - "enum": [ - "auto", - "caseInsensitive", - "caseSensitive" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseSensitivity.auto%", - "%typescript.preferences.organizeImports.caseSensitivity.insensitive", - "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" - ], - "default": "auto" - }, - "typeOrder": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", - "enum": [ - "auto", - "last", - "inline", - "first" - ], - "default": "auto", - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.typeOrder.auto%", - "%typescript.preferences.organizeImports.typeOrder.last%", - "%typescript.preferences.organizeImports.typeOrder.inline%", - "%typescript.preferences.organizeImports.typeOrder.first%" - ] - }, - "unicodeCollation": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", - "enum": [ - "ordinal", - "unicode" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", - "%typescript.preferences.organizeImports.unicodeCollation.unicode%" - ], - "default": "ordinal" - }, - "locale": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.locale%" - }, - "numericCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" - }, - "accentCollation": { - "type": "boolean", - "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" - }, - "caseFirst": { - "type": "string", - "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", - "enum": [ - "default", - "upper", - "lower" - ], - "markdownEnumDescriptions": [ - "%typescript.preferences.organizeImports.caseFirst.default%", - "%typescript.preferences.organizeImports.caseFirst.upper%", - "%typescript.preferences.organizeImports.caseFirst.lower%" - ], - "default": "default" - } + } + }, + { + "type": "object", + "title": "%configuration.typescript.suggest%", + "order": 21, + "properties": { + "typescript.suggest.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggest.enabled%", + "scope": "language-overridable" + }, + "typescript.suggest.autoImports": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.autoImports%", + "scope": "resource" + }, + "typescript.suggest.completeFunctionCalls": { + "type": "boolean", + "default": false, + "description": "%configuration.suggest.completeFunctionCalls%", + "scope": "resource" + }, + "typescript.suggest.paths": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.paths%", + "scope": "resource" + }, + "typescript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "scope": "language-overridable" + }, + "typescript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "scope": "language-overridable" + }, + "typescript.suggest.includeAutomaticOptionalChainCompletions": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "scope": "resource" + }, + "typescript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "scope": "resource" + }, + "typescript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "scope": "resource" + }, + "typescript.suggest.objectLiteralMethodSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", + "scope": "resource" } - }, - "typescript.updateImportsOnFileMove.enabled": { - "type": "string", - "enum": [ - "prompt", - "always", - "never" - ], - "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" - ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "scope": "resource" - }, - "javascript.updateImportsOnFileMove.enabled": { - "type": "string", - "enum": [ - "prompt", - "always", - "never" - ], - "markdownEnumDescriptions": [ - "%typescript.updateImportsOnFileMove.enabled.prompt%", - "%typescript.updateImportsOnFileMove.enabled.always%", - "%typescript.updateImportsOnFileMove.enabled.never%" - ], - "default": "prompt", - "description": "%typescript.updateImportsOnFileMove.enabled%", - "scope": "resource" - }, - "typescript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "scope": "language-overridable" - }, - "javascript.autoClosingTags": { - "type": "boolean", - "default": true, - "description": "%typescript.autoClosingTags%", - "scope": "language-overridable" - }, - "javascript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "scope": "language-overridable" - }, - "typescript.suggest.enabled": { - "type": "boolean", - "default": true, - "description": "%typescript.suggest.enabled%", - "scope": "language-overridable" - }, - "typescript.tsserver.useSeparateSyntaxServer": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.useSeparateSyntaxServer%", - "markdownDeprecationMessage": "%configuration.tsserver.useSeparateSyntaxServer.deprecation%", - "scope": "window" - }, - "typescript.tsserver.useSyntaxServer": { - "type": "string", - "scope": "window", - "description": "%configuration.tsserver.useSyntaxServer%", - "default": "auto", - "enum": [ - "always", - "never", - "auto" - ], - "enumDescriptions": [ - "%configuration.tsserver.useSyntaxServer.always%", - "%configuration.tsserver.useSyntaxServer.never%", - "%configuration.tsserver.useSyntaxServer.auto%" - ] - }, - "typescript.tsserver.maxTsServerMemory": { - "type": "number", - "default": 3072, - "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", - "scope": "window" - }, - "typescript.tsserver.experimental.enableProjectDiagnostics": { - "type": "boolean", - "default": false, - "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", - "scope": "window", - "tags": [ - "experimental" - ] - }, - "typescript.tsserver.experimental.useVsCodeWatcher": { - "type": "boolean", - "description": "%configuration.tsserver.useVsCodeWatcher%", - "deprecationMessage": "%configuration.tsserver.useVsCodeWatcher.deprecation%", - "default": true - }, - "typescript.tsserver.watchOptions": { - "description": "%configuration.tsserver.watchOptions%", - "scope": "window", - "default": "vscode", - "oneOf": [ - { - "type": "string", - "const": "vscode", - "description": "%configuration.tsserver.watchOptions.vscode%" + } + }, + { + "type": "object", + "title": "%configuration.javascript.suggest%", + "order": 21, + "properties": { + "javascript.suggest.enabled": { + "type": "boolean", + "default": true, + "description": "%typescript.suggest.enabled%", + "scope": "language-overridable" + }, + "javascript.suggest.autoImports": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.autoImports%", + "scope": "resource" + }, + "javascript.suggest.names": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.names%", + "scope": "resource" + }, + "javascript.suggest.paths": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.paths%", + "scope": "resource" + }, + "javascript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.completeJSDocs%", + "scope": "language-overridable" + }, + "javascript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.suggest.jsdoc.generateReturns%", + "scope": "language-overridable" + }, + "javascript.suggest.completeFunctionCalls": { + "type": "boolean", + "default": false, + "description": "%configuration.suggest.completeFunctionCalls%", + "scope": "resource" + }, + "javascript.suggest.includeAutomaticOptionalChainCompletions": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "scope": "resource" + }, + "javascript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.includeCompletionsForImportStatements%", + "scope": "resource" + }, + "javascript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.suggest.classMemberSnippets.enabled%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.typescript.preferences%", + "order": 21, + "properties": { + "typescript.preferences.quoteStyle": { + "type": "string", + "enum": [ + "auto", + "single", + "double" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", + "markdownEnumDescriptions": [ + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" + ], + "scope": "language-overridable" + }, + "typescript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "scope": "language-overridable" + }, + "typescript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "language-overridable" + }, + "typescript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "scope": "language-overridable" + }, + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "%typescript.preferences.includePackageJsonAutoImports.auto%", + "%typescript.preferences.includePackageJsonAutoImports.on%", + "%typescript.preferences.includePackageJsonAutoImports.off%" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%", + "scope": "window" + }, + "typescript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" }, - { - "type": "object", - "properties": { - "watchFile": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.watchFile%", - "enum": [ - "fixedChunkSizePolling", - "fixedPollingInterval", - "priorityPollingInterval", - "dynamicPriorityPolling", - "useFsEvents", - "useFsEventsOnParentDirectory" - ], - "enumDescriptions": [ - "%configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling%", - "%configuration.tsserver.watchOptions.watchFile.fixedPollingInterval%", - "%configuration.tsserver.watchOptions.watchFile.priorityPollingInterval%", - "%configuration.tsserver.watchOptions.watchFile.dynamicPriorityPolling%", - "%configuration.tsserver.watchOptions.watchFile.useFsEvents%", - "%configuration.tsserver.watchOptions.watchFile.useFsEventsOnParentDirectory%" - ], - "default": "useFsEvents" - }, - "watchDirectory": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.watchDirectory%", - "enum": [ - "fixedChunkSizePolling", - "fixedPollingInterval", - "dynamicPriorityPolling", - "useFsEvents" - ], - "enumDescriptions": [ - "%configuration.tsserver.watchOptions.watchDirectory.fixedChunkSizePolling%", - "%configuration.tsserver.watchOptions.watchDirectory.fixedPollingInterval%", - "%configuration.tsserver.watchOptions.watchDirectory.dynamicPriorityPolling%", - "%configuration.tsserver.watchOptions.watchDirectory.useFsEvents%" - ], - "default": "useFsEvents" - }, - "fallbackPolling": { - "type": "string", - "description": "%configuration.tsserver.watchOptions.fallbackPolling%", - "enum": [ - "fixedPollingInterval", - "priorityPollingInterval", - "dynamicPriorityPolling" - ], - "enumDescriptions": [ - "configuration.tsserver.watchOptions.fallbackPolling.fixedPollingInterval", - "configuration.tsserver.watchOptions.fallbackPolling.priorityPollingInterval", - "configuration.tsserver.watchOptions.fallbackPolling.dynamicPriorityPolling" - ] - }, - "synchronousWatchDirectory": { - "type": "boolean", - "description": "%configuration.tsserver.watchOptions.synchronousWatchDirectory%" - } + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "scope": "resource" + }, + "typescript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "scope": "resource" + }, + "typescript.preferences.preferTypeOnlyAutoImports": { + "type": "boolean", + "default": false, + "markdownDescription": "%typescript.preferences.preferTypeOnlyAutoImports%", + "scope": "resource" + }, + "typescript.preferences.renameShorthandProperties": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", + "scope": "language-overridable" + }, + "typescript.preferences.useAliasesForRenames": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "scope": "language-overridable" + }, + "typescript.preferences.renameMatchingJsxTags": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.renameMatchingJsxTags%", + "scope": "language-overridable" + }, + "typescript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" } } - ] - }, - "typescript.workspaceSymbols.scope": { - "type": "string", - "enum": [ - "allOpenProjects", - "currentProject" - ], - "enumDescriptions": [ - "%typescript.workspaceSymbols.scope.allOpenProjects%", - "%typescript.workspaceSymbols.scope.currentProject%" - ], - "default": "allOpenProjects", - "markdownDescription": "%typescript.workspaceSymbols.scope%", - "scope": "window" - }, - "javascript.suggest.classMemberSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "scope": "resource" - }, - "typescript.suggest.classMemberSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.classMemberSnippets.enabled%", - "scope": "resource" - }, - "typescript.suggest.objectLiteralMethodSnippets.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", - "scope": "resource" - }, - "typescript.tsserver.web.projectWideIntellisense.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.web.projectWideIntellisense.enabled%", - "scope": "window" - }, - "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { - "type": "boolean", - "default": false, - "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", - "scope": "window" - }, - "typescript.tsserver.web.typeAcquisition.enabled": { - "type": "boolean", - "default": true, - "description": "%configuration.tsserver.web.typeAcquisition.enabled%", - "scope": "window" - }, - "typescript.tsserver.nodePath": { - "type": "string", - "description": "%configuration.tsserver.nodePath%", - "scope": "window" - }, - "typescript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "scope": "window" - }, - "javascript.preferGoToSourceDefinition": { - "type": "boolean", - "default": false, - "description": "%configuration.preferGoToSourceDefinition%", - "scope": "window" - }, - "typescript.workspaceSymbols.excludeLibrarySymbols": { - "type": "boolean", - "default": true, - "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", - "scope": "window" - }, - "typescript.tsserver.enableRegionDiagnostics": { - "type": "boolean", - "default": true, - "description": "%typescript.tsserver.enableRegionDiagnostics%", - "scope": "window" - }, - "javascript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%" - }, - "typescript.updateImportsOnPaste.enabled": { - "scope": "window", - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.updateImportsOnPaste%" + } + } + }, + { + "type": "object", + "title": "%configuration.javascript.preferences%", + "order": 22, + "properties": { + "javascript.preferences.quoteStyle": { + "type": "string", + "enum": [ + "auto", + "single", + "double" + ], + "default": "auto", + "markdownDescription": "%typescript.preferences.quoteStyle%", + "markdownEnumDescriptions": [ + "%typescript.preferences.quoteStyle.auto%", + "%typescript.preferences.quoteStyle.single%", + "%typescript.preferences.quoteStyle.double%" + ], + "scope": "language-overridable" + }, + "javascript.preferences.importModuleSpecifier": { + "type": "string", + "enum": [ + "shortest", + "relative", + "non-relative", + "project-relative" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifier.shortest%", + "%typescript.preferences.importModuleSpecifier.relative%", + "%typescript.preferences.importModuleSpecifier.nonRelative%", + "%typescript.preferences.importModuleSpecifier.projectRelative%" + ], + "default": "shortest", + "description": "%typescript.preferences.importModuleSpecifier%", + "scope": "language-overridable" + }, + "javascript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "enumItemLabels": [ + null, + null, + null, + "%typescript.preferences.importModuleSpecifierEnding.label.js%" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "language-overridable" + }, + "javascript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "%javascript.preferences.jsxAttributeCompletionStyle.auto%", + "%typescript.preferences.jsxAttributeCompletionStyle.braces%", + "%typescript.preferences.jsxAttributeCompletionStyle.none%" + ], + "default": "auto", + "description": "%typescript.preferences.jsxAttributeCompletionStyle%", + "scope": "language-overridable" + }, + "javascript.preferences.autoImportFileExcludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%", + "scope": "resource" + }, + "javascript.preferences.autoImportSpecifierExcludeRegexes": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "%typescript.preferences.autoImportSpecifierExcludeRegexes%", + "scope": "resource" + }, + "javascript.preferences.renameShorthandProperties": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "deprecationMessage": "%typescript.preferences.renameShorthandProperties.deprecationMessage%", + "scope": "language-overridable" + }, + "javascript.preferences.useAliasesForRenames": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.useAliasesForRenames%", + "scope": "language-overridable" + }, + "javascript.preferences.renameMatchingJsxTags": { + "type": "boolean", + "default": true, + "description": "%typescript.preferences.renameMatchingJsxTags%", + "scope": "language-overridable" + }, + "javascript.preferences.organizeImports": { + "type": "object", + "markdownDescription": "%typescript.preferences.organizeImports%", + "properties": { + "caseSensitivity": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseSensitivity%", + "enum": [ + "auto", + "caseInsensitive", + "caseSensitive" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseSensitivity.auto%", + "%typescript.preferences.organizeImports.caseSensitivity.insensitive", + "%typescript.preferences.organizeImports.caseSensitivity.sensitive%" + ], + "default": "auto" + }, + "typeOrder": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.typeOrder%", + "enum": [ + "auto", + "last", + "inline", + "first" + ], + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] + }, + "unicodeCollation": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.unicodeCollation%", + "enum": [ + "ordinal", + "unicode" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.unicodeCollation.ordinal%", + "%typescript.preferences.organizeImports.unicodeCollation.unicode%" + ], + "default": "ordinal" + }, + "locale": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.locale%" + }, + "numericCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.numericCollation%" + }, + "accentCollation": { + "type": "boolean", + "markdownDescription": "%typescript.preferences.organizeImports.accentCollation%" + }, + "caseFirst": { + "type": "string", + "markdownDescription": "%typescript.preferences.organizeImports.caseFirst%", + "enum": [ + "default", + "upper", + "lower" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.caseFirst.default%", + "%typescript.preferences.organizeImports.caseFirst.upper%", + "%typescript.preferences.organizeImports.caseFirst.lower%" + ], + "default": "default" + } + } + } + } + }, + { + "type": "object", + "title": "%configuration.typescript.format%", + "order": 23, + "properties": { + "typescript.format.enable": { + "type": "boolean", + "default": true, + "description": "%typescript.format.enable%", + "scope": "window" + }, + "typescript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "scope": "resource" + }, + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "scope": "resource" + }, + "typescript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "scope": "resource" + }, + "typescript.format.insertSpaceAfterTypeAssertion": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterTypeAssertion%", + "scope": "resource" + }, + "typescript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "scope": "resource" + }, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "scope": "resource" + }, + "typescript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] + }, + "typescript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.javascript.format%", + "order": 24, + "properties": { + "javascript.format.enable": { + "type": "boolean", + "default": true, + "description": "%javascript.format.enable%", + "scope": "window" + }, + "javascript.format.insertSpaceAfterCommaDelimiter": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterCommaDelimiter%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterConstructor": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterConstructor%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterSemicolonInForStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterSemicolonInForStatements%", + "scope": "resource" + }, + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceBeforeAndAfterBinaryOperators%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterKeywordsInControlFlowStatements%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterFunctionKeywordForAnonymousFunctions%", + "scope": "resource" + }, + "javascript.format.insertSpaceBeforeFunctionParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceBeforeFunctionParenthesis%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": { + "type": "boolean", + "default": true, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces%", + "scope": "resource" + }, + "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": { + "type": "boolean", + "default": false, + "description": "%format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces%", + "scope": "resource" + }, + "javascript.format.placeOpenBraceOnNewLineForFunctions": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForFunctions%", + "scope": "resource" + }, + "javascript.format.placeOpenBraceOnNewLineForControlBlocks": { + "type": "boolean", + "default": false, + "description": "%format.placeOpenBraceOnNewLineForControlBlocks%", + "scope": "resource" + }, + "javascript.format.semicolons": { + "type": "string", + "default": "ignore", + "description": "%format.semicolons%", + "scope": "resource", + "enum": [ + "ignore", + "insert", + "remove" + ], + "enumDescriptions": [ + "%format.semicolons.ignore%", + "%format.semicolons.insert%", + "%format.semicolons.remove%" + ] + }, + "javascript.format.indentSwitchCase": { + "type": "boolean", + "default": true, + "description": "%format.indentSwitchCase%", + "scope": "resource" + } + } + }, + { + "type": "object", + "title": "%configuration.server%", + "order": 25, + "properties": { + "typescript.tsserver.nodePath": { + "type": "string", + "description": "%configuration.tsserver.nodePath%", + "scope": "window" + }, + "typescript.npm": { + "type": "string", + "markdownDescription": "%typescript.npm%", + "scope": "machine" + }, + "typescript.check.npmIsInstalled": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.check.npmIsInstalled%", + "scope": "window" + }, + "typescript.tsserver.web.projectWideIntellisense.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.projectWideIntellisense.enabled%", + "scope": "window" + }, + "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", + "scope": "window" + }, + "typescript.tsserver.web.typeAcquisition.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.typeAcquisition.enabled%", + "scope": "window" + }, + "typescript.tsserver.useSeparateSyntaxServer": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.useSeparateSyntaxServer%", + "markdownDeprecationMessage": "%configuration.tsserver.useSeparateSyntaxServer.deprecation%", + "scope": "window" + }, + "typescript.tsserver.useSyntaxServer": { + "type": "string", + "scope": "window", + "description": "%configuration.tsserver.useSyntaxServer%", + "default": "auto", + "enum": [ + "always", + "never", + "auto" + ], + "enumDescriptions": [ + "%configuration.tsserver.useSyntaxServer.always%", + "%configuration.tsserver.useSyntaxServer.never%", + "%configuration.tsserver.useSyntaxServer.auto%" + ] + }, + "typescript.tsserver.maxTsServerMemory": { + "type": "number", + "default": 3072, + "markdownDescription": "%configuration.tsserver.maxTsServerMemory%", + "scope": "window" + }, + "typescript.tsserver.experimental.enableProjectDiagnostics": { + "type": "boolean", + "default": false, + "description": "%configuration.tsserver.experimental.enableProjectDiagnostics%", + "scope": "window", + "tags": [ + "experimental" + ] + }, + "typescript.tsserver.experimental.useVsCodeWatcher": { + "type": "boolean", + "description": "%configuration.tsserver.useVsCodeWatcher%", + "deprecationMessage": "%configuration.tsserver.useVsCodeWatcher.deprecation%", + "default": true + }, + "typescript.tsserver.watchOptions": { + "description": "%configuration.tsserver.watchOptions%", + "scope": "window", + "default": "vscode", + "oneOf": [ + { + "type": "string", + "const": "vscode", + "description": "%configuration.tsserver.watchOptions.vscode%" + }, + { + "type": "object", + "properties": { + "watchFile": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.watchFile%", + "enum": [ + "fixedChunkSizePolling", + "fixedPollingInterval", + "priorityPollingInterval", + "dynamicPriorityPolling", + "useFsEvents", + "useFsEventsOnParentDirectory" + ], + "enumDescriptions": [ + "%configuration.tsserver.watchOptions.watchFile.fixedChunkSizePolling%", + "%configuration.tsserver.watchOptions.watchFile.fixedPollingInterval%", + "%configuration.tsserver.watchOptions.watchFile.priorityPollingInterval%", + "%configuration.tsserver.watchOptions.watchFile.dynamicPriorityPolling%", + "%configuration.tsserver.watchOptions.watchFile.useFsEvents%", + "%configuration.tsserver.watchOptions.watchFile.useFsEventsOnParentDirectory%" + ], + "default": "useFsEvents" + }, + "watchDirectory": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.watchDirectory%", + "enum": [ + "fixedChunkSizePolling", + "fixedPollingInterval", + "dynamicPriorityPolling", + "useFsEvents" + ], + "enumDescriptions": [ + "%configuration.tsserver.watchOptions.watchDirectory.fixedChunkSizePolling%", + "%configuration.tsserver.watchOptions.watchDirectory.fixedPollingInterval%", + "%configuration.tsserver.watchOptions.watchDirectory.dynamicPriorityPolling%", + "%configuration.tsserver.watchOptions.watchDirectory.useFsEvents%" + ], + "default": "useFsEvents" + }, + "fallbackPolling": { + "type": "string", + "description": "%configuration.tsserver.watchOptions.fallbackPolling%", + "enum": [ + "fixedPollingInterval", + "priorityPollingInterval", + "dynamicPriorityPolling" + ], + "enumDescriptions": [ + "configuration.tsserver.watchOptions.fallbackPolling.fixedPollingInterval", + "configuration.tsserver.watchOptions.fallbackPolling.priorityPollingInterval", + "configuration.tsserver.watchOptions.fallbackPolling.dynamicPriorityPolling" + ] + }, + "synchronousWatchDirectory": { + "type": "boolean", + "description": "%configuration.tsserver.watchOptions.synchronousWatchDirectory%" + } + } + } + ] + }, + "typescript.tsserver.enableTracing": { + "type": "boolean", + "default": false, + "description": "%typescript.tsserver.enableTracing%", + "scope": "window" + }, + "typescript.tsserver.log": { + "type": "string", + "enum": [ + "off", + "terse", + "normal", + "verbose" + ], + "default": "off", + "description": "%typescript.tsserver.log%", + "scope": "window" + }, + "typescript.tsserver.pluginPaths": { + "type": "array", + "items": { + "type": "string", + "description": "%typescript.tsserver.pluginPaths.item%" + }, + "default": [], + "description": "%typescript.tsserver.pluginPaths%", + "scope": "machine" + } } } - }, + ], "commands": [ { "command": "typescript.reloadProjects", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 447359e11a5..abbc9896ce3 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -5,6 +5,13 @@ "virtualWorkspaces": "In virtual workspaces, resolving and finding references across files is not supported.", "reloadProjects.title": "Reload Project", "configuration.typescript": "TypeScript", + "configuration.javascript.preferences": "JavaScript Preferences", + "configuration.typescript.preferences": "TypeScript Preferences", + "configuration.javascript.format": "JavaScript Formatting", + "configuration.typescript.format": "TypeScript Formatting", + "configuration.javascript.suggest": "JavaScript Suggestions", + "configuration.typescript.suggest": "TypeScript Suggestions", + "configuration.server": "TS Server", "configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.", "configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.", "configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.", diff --git a/extensions/typescript-language-features/src/commands/tsserverRequests.ts b/extensions/typescript-language-features/src/commands/tsserverRequests.ts index 0fc70b33bf5..7c925024b96 100644 --- a/extensions/typescript-language-features/src/commands/tsserverRequests.ts +++ b/extensions/typescript-language-features/src/commands/tsserverRequests.ts @@ -16,6 +16,7 @@ function isCancellationToken(value: any): value is vscode.CancellationToken { interface RequestArgs { readonly file?: unknown; + readonly $traceId?: unknown; } export class TSServerRequestCommand implements Command { @@ -31,11 +32,18 @@ export class TSServerRequestCommand implements Command { } if (args && typeof args === 'object' && !Array.isArray(args)) { const requestArgs = args as RequestArgs; - let newArgs: any = undefined; - if (requestArgs.file instanceof vscode.Uri) { - newArgs = { ...args }; - const client = this.lazyClientHost.value.serviceClient; - newArgs.file = client.toOpenTsFilePath(requestArgs.file); + const hasFile = requestArgs.file instanceof vscode.Uri; + const hasTraceId = typeof requestArgs.$traceId === 'string'; + if (hasFile || hasTraceId) { + const newArgs = { ...args }; + if (hasFile) { + const client = this.lazyClientHost.value.serviceClient; + newArgs.file = client.toOpenTsFilePath(requestArgs.file); + } + if (hasTraceId) { + const telemetryReporter = this.lazyClientHost.value.serviceClient.telemetryReporter; + telemetryReporter.logTraceEvent('TSServerRequestCommand.execute', requestArgs.$traceId, JSON.stringify({ command })); + } args = newArgs; } } diff --git a/extensions/typescript-language-features/src/logging/telemetry.ts b/extensions/typescript-language-features/src/logging/telemetry.ts index 4b2d3867f6d..b96487b2419 100644 --- a/extensions/typescript-language-features/src/logging/telemetry.ts +++ b/extensions/typescript-language-features/src/logging/telemetry.ts @@ -11,6 +11,7 @@ export interface TelemetryProperties { export interface TelemetryReporter { logTelemetry(eventName: string, properties?: TelemetryProperties): void; + logTraceEvent(tracePoint: string, correlationId: string, command?: string): void; } export class VSCodeTelemetryReporter implements TelemetryReporter { @@ -34,4 +35,27 @@ export class VSCodeTelemetryReporter implements TelemetryReporter { reporter.postEventObj(eventName, properties); } + + public logTraceEvent(point: string, id: string, data?: string): void { + const event: { point: string; id: string; data?: string | undefined } = { + point, + id + }; + if (data) { + event.data = data; + } + + /* __GDPR__ + "typeScriptExtension.trace" : { + "owner": "dirkb", + "${include}": [ + "${TypeScriptCommonProperties}" + ], + "point" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The trace point." }, + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The traceId is used to correlate the request with other trace points." }, + "data": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Additional data" } + } + */ + this.logTelemetry('typeScriptExtension.trace', event); + } } diff --git a/extensions/typescript-language-features/src/test/unit/server.test.ts b/extensions/typescript-language-features/src/test/unit/server.test.ts index 4d086be5b88..ae4a05a6555 100644 --- a/extensions/typescript-language-features/src/test/unit/server.test.ts +++ b/extensions/typescript-language-features/src/test/unit/server.test.ts @@ -18,6 +18,7 @@ import { nulToken } from '../../utils/cancellation'; const NoopTelemetryReporter = new class implements TelemetryReporter { logTelemetry(): void { /* noop */ } + logTraceEvent(): void { /* noop */ } dispose(): void { /* noop */ } }; diff --git a/extensions/typescript-language-features/src/tsServer/callbackMap.ts b/extensions/typescript-language-features/src/tsServer/callbackMap.ts index 57a80051e6d..1484b0ff654 100644 --- a/extensions/typescript-language-features/src/tsServer/callbackMap.ts +++ b/extensions/typescript-language-features/src/tsServer/callbackMap.ts @@ -11,6 +11,7 @@ export interface CallbackItem { readonly onError: (err: Error) => void; readonly queuingStartTime: number; readonly isAsync: boolean; + readonly traceId?: string | undefined; } export class CallbackMap { @@ -43,6 +44,10 @@ export class CallbackMap { return callback; } + public peek(seq: number): CallbackItem | undefined> | undefined { + return this._callbacks.get(seq) ?? this._asyncCallbacks.get(seq); + } + private delete(seq: number) { if (!this._callbacks.delete(seq)) { this._asyncCallbacks.delete(seq); diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 4e41f7aa79a..dbb867f8bb3 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -185,6 +185,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { private tryCancelRequest(request: Proto.Request, command: string): boolean { const seq = request.seq; + const callback = this._callbacks.peek(seq); + if (callback?.traceId !== undefined) { + this._telemetryReporter.logTraceEvent('TSServer.tryCancelRequest', callback.traceId, JSON.stringify({ command, cancelled: true })); + } try { if (this._requestQueue.tryDeletePendingRequest(seq)) { this.logTrace(`Canceled request with sequence number ${seq}`); @@ -206,7 +210,9 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { if (!callback) { return; } - + if (callback.traceId !== undefined) { + this._telemetryReporter.logTraceEvent('TSServerRequest.dispatchResponse', callback.traceId, JSON.stringify({ command: response.command, success: response.success, performanceData: response.performanceData })); + } this._tracer.traceResponse(this._serverId, response, callback); if (response.success) { callback.onSuccess(response); @@ -218,7 +224,7 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { } } - public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean; token?: vscode.CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget }): Array> | undefined> { + public executeImpl(command: keyof TypeScriptRequests, args: unknown, executeInfo: { isAsync: boolean; token?: vscode.CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget }): Array> | undefined> { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -229,7 +235,7 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { let result: Promise> | undefined; if (executeInfo.expectsResult) { result = new Promise>((resolve, reject) => { - this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync); + this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync, traceId: request.arguments?.$traceId }, executeInfo.isAsync); if (executeInfo.token) { @@ -263,6 +269,10 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { } this._requestQueue.enqueue(requestInfo); + if (args && typeof (args as any).$traceId === 'string') { + const queueLength = this._requestQueue.length - 1; + this._telemetryReporter.logTraceEvent('TSServer.enqueueRequest', (args as any).$traceId, JSON.stringify({ command, queueLength })); + } this.sendNextRequests(); return [result]; @@ -287,6 +297,9 @@ export class SingleTsServer extends Disposable implements ITypeScriptServer { try { this.write(serverRequest); + if (typeof serverRequest.arguments?.$traceId === 'string') { + this._telemetryReporter.logTraceEvent('TSServer.sendRequest', serverRequest.arguments.$traceId, JSON.stringify({ command: serverRequest.command })); + } } catch (err) { const callback = this.fetchCallback(serverRequest.seq); callback?.onError(err); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 289f0a6a1c0..df90a035401 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { ChatContext, ChatRequest, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; +import { ChatContext, ChatRequest, ChatRequestTurn, ChatRequestTurn2, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils'; suite('chat', () => { @@ -71,6 +71,7 @@ suite('chat', () => { assert.strictEqual(request.context.history.length, 2); assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].command, 'hello'); + assert.ok(request.context.history[0] instanceof ChatRequestTurn && request.context.history[0] instanceof ChatRequestTurn2); deferred.complete(); } } catch (e) { diff --git a/package-lock.json b/package-lock.json index 42959bae1cb..beb023f21d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -28,16 +27,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -96,8 +95,8 @@ "css-loader": "^6.9.1", "cssnano": "^6.0.3", "debounce": "^1.0.0", - "deemon": "^1.13.2", - "electron": "34.4.1", + "deemon": "^1.13.4", + "electron": "34.5.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -155,7 +154,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.9.0-dev.20250324", + "typescript": "^5.9.0-dev.20250416", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -793,18 +792,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@c4312/eventsource-umd": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", - "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3325,30 +3312,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3358,64 +3345,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", - "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", + "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -5609,9 +5596,9 @@ } }, "node_modules/deemon": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.2.tgz", - "integrity": "sha512-baAEhezwHtouoi4mpSvb3v2yRsSDxJDekjBj/A8SKV2AWF4sPez7zVW4OMka9tTMKPwaQzyp+5L+PoW+ihX96w==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.4.tgz", + "integrity": "sha512-O+7MRrNEddXeZXJusSSkFfBsJ5faVt+XpjfIosciuaK6StTjMi5Q4poYUxFlrIPHusrhrc4isC1gxt9nLijf6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6015,9 +6002,9 @@ "dev": true }, "node_modules/electron": { - "version": "34.4.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.4.1.tgz", - "integrity": "sha512-iYzeLBdCrAR3i0RVSLa+mzuFZwH6HGxTGKsI+SS41sg2anZj4R5mHjOiHsxcZ50/ih47NJbuVRJgPIVlTF+USg==", + "version": "34.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.1.tgz", + "integrity": "sha512-z2Wm7QjhnJ5592fLITynj8UwIk1mBiT402mOakxSYiADrERIci3IOPk7xWHAFOMvt/eoG5RW16PPhgJiedZcGA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6603,15 +6590,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -11025,9 +11003,9 @@ } }, "node_modules/koa": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.4.tgz", - "integrity": "sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "dev": true, "license": "MIT", "dependencies": { @@ -17455,9 +17433,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.9.0-dev.20250324", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250324.tgz", - "integrity": "sha512-qehVULhODb1IphPefX/jq0wAXpk6ifAt2lF3mxTsyR3yPDoPkiQdvxw9cURaDjJw6vCijSER6dU8kWmHdWaQnA==", + "version": "5.9.0-dev.20250416", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250416.tgz", + "integrity": "sha512-NCJUSWqeGImKoCTOydww1MhHvtNjU5GmhJ5LAhqjvZOYcHJSO0DfYSKb39klxfwZoHS9aNgay9bVIWbfODpCUA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index c19ff829b39..30378836bb1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.100.0", - "distro": "d80140b92402889bdc3d892e1cb7a52718bd99fe", + "distro": "47d9c6291ce9549954bbf119a3874d724e55884f", "author": { "name": "Microsoft Corporation" }, @@ -69,7 +69,6 @@ "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" }, "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -87,16 +86,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -155,8 +154,8 @@ "css-loader": "^6.9.1", "cssnano": "^6.0.3", "debounce": "^1.0.0", - "deemon": "^1.13.2", - "electron": "34.4.1", + "deemon": "^1.13.4", + "electron": "34.5.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -214,7 +213,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^5.9.0-dev.20250324", + "typescript": "^5.9.0-dev.20250416", "typescript-eslint": "^8.8.0", "util": "^0.12.4", "webpack": "^5.94.0", diff --git a/remote/.npmrc b/remote/.npmrc index e2c53927b15..3f17dea3fd9 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="20.18.3" -ms_build_id="323695" +target="20.19.0" +ms_build_id="332907" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/remote/package-lock.json b/remote/package-lock.json index f9921d3e521..a99aa799f69 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,7 +8,6 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -21,16 +20,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -47,18 +46,6 @@ "yazl": "^2.4.3" } }, - "node_modules/@c4312/eventsource-umd": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", - "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@microsoft/1ds-core-js": { "version": "3.2.13", "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", @@ -536,30 +523,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -569,64 +556,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", - "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.101.tgz", + "integrity": "sha512-uAIo1b50keq3Ybps3Q5QcakVz570hY7gdU/71v52N2BxbvXy0wPk92Q4lCapsKmdtQ3+HbLtRsh1s4k0oP4VGw==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/agent-base": { @@ -787,15 +774,6 @@ "once": "^1.4.0" } }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", diff --git a/remote/package.json b/remote/package.json index 203f38da292..e15e4dce8e2 100644 --- a/remote/package.json +++ b/remote/package.json @@ -3,7 +3,6 @@ "version": "0.0.0", "private": true, "dependencies": { - "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", @@ -16,16 +15,16 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/headless": "^5.6.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/headless": "^5.6.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 63910bd2bb6..a6c826c2a73 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -90,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.82", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", - "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", + "version": "0.2.0-beta.84", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.84.tgz", + "integrity": "sha512-/7lRpyLboTDKa1SMQCkLkUnH5hawiDsZ1VDMhfgjEr44ltw3cv2YuTtPQYkKen0vfu/0uzZeHWCwsZpQK25nRA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", - "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.101.tgz", + "integrity": "sha512-iAp4DFxqEhN1DWcCy3d66NgrAklKXfZhHlE8T0rvGS1mfK8ubO5WODXUdMO0rwU5TSrnt4l21DVwFhSs+2oWQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", - "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", + "version": "0.10.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.101.tgz", + "integrity": "sha512-QgixJpyzP4ZFhv0YJJgNFXih7escNod9cGTAG7eW/dYwnunZwSmi7Bal/u3m6IC5SZbjAAOjKBGZyfvHefK7SA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -123,58 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.5", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", - "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", + "version": "0.2.0-beta.7", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.7.tgz", + "integrity": "sha512-1FrJcHm2R+s7auGTrb3rzTevFz5nTP8dLHmY24iVq1a3rPxrprCkfDkugQJsCNG0rd5GT1qk9YWjVcu3GO7gQw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", - "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", + "version": "0.16.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.101.tgz", + "integrity": "sha512-rrT9KQsQb/OUQwSVvAIKNFslEM2ux6824GZYPB6uYJbFkRwI+aGKiqs8UM264UcZotHylMSg3dYybGPBImTH3A==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", - "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", + "version": "0.14.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.101.tgz", + "integrity": "sha512-LOJtJroDjoHY9EhSAr0UuWZ59bnZFnZ73xvBT0AyEH0Oqd7MC0LZtI0oV4ifcQU90Eb1oDq3LRfgHm9vAtUrFg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", - "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", + "version": "0.9.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.101.tgz", + "integrity": "sha512-iu1Ry7im8NO3hITbYHbsxZKTxiJQSvg/tGR1EXK1lFIXe9gHc6bqTQPhvFYZ8xgPNt+V1AHZY9SpwcxgBOuxUA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", - "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", + "version": "0.19.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.101.tgz", + "integrity": "sha512-g9YzOEqYS7MW1QirNRQhUsRJeFKxsksVQ6iT1dOScjZg7DRwil7/HNS03hQkgigW2Ku3hP5hK0WdXDe4np20gQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.99" + "@xterm/xterm": "^5.6.0-beta.101" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.99", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", - "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", + "version": "5.6.0-beta.101", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.101.tgz", + "integrity": "sha512-kV6Ad/KTcCgKWTYshufBEfT3OyadFLuskW+R+nJIJKrlAB34vRsX7TXFJ0P9QoMAeqXQpgngDfTn+RTAESyVyw==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 1ca215cc76e..167d8e4dbba 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.4", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.82", - "@xterm/addon-image": "^0.9.0-beta.99", - "@xterm/addon-ligatures": "^0.10.0-beta.99", - "@xterm/addon-progress": "^0.2.0-beta.5", - "@xterm/addon-search": "^0.16.0-beta.99", - "@xterm/addon-serialize": "^0.14.0-beta.99", - "@xterm/addon-unicode11": "^0.9.0-beta.99", - "@xterm/addon-webgl": "^0.19.0-beta.99", - "@xterm/xterm": "^5.6.0-beta.99", + "@xterm/addon-clipboard": "^0.2.0-beta.84", + "@xterm/addon-image": "^0.9.0-beta.101", + "@xterm/addon-ligatures": "^0.10.0-beta.101", + "@xterm/addon-progress": "^0.2.0-beta.7", + "@xterm/addon-search": "^0.16.0-beta.101", + "@xterm/addon-serialize": "^0.14.0-beta.101", + "@xterm/addon-unicode11": "^0.9.0-beta.101", + "@xterm/addon-webgl": "^0.19.0-beta.101", + "@xterm/xterm": "^5.6.0-beta.101", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts index 1e6f8b3032a..3cad09d7dcc 100644 --- a/src/vs/base/browser/mouseEvent.ts +++ b/src/vs/base/browser/mouseEvent.ts @@ -22,6 +22,7 @@ export interface IMouseEvent { readonly altKey: boolean; readonly metaKey: boolean; readonly timestamp: number; + readonly defaultPrevented: boolean; preventDefault(): void; stopPropagation(): void; @@ -44,6 +45,7 @@ export class StandardMouseEvent implements IMouseEvent { public readonly altKey: boolean; public readonly metaKey: boolean; public readonly timestamp: number; + public readonly defaultPrevented: boolean; constructor(targetWindow: Window, e: MouseEvent) { this.timestamp = Date.now(); @@ -52,6 +54,7 @@ export class StandardMouseEvent implements IMouseEvent { this.middleButton = e.button === 1; this.rightButton = e.button === 2; this.buttons = e.buttons; + this.defaultPrevented = e.defaultPrevented; this.target = e.target; diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index 6b877ec17fd..6c682c3c083 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -325,6 +325,13 @@ export interface IHoverAppearanceOptions { * another in the same group so it looks like the hover is moving from one element to the other. */ skipFadeInAnimation?: boolean; + + /** + * The max height of the hover relative to the window height. + * Accepted values: (0,1] + * Default: 0.5 + */ + maxHeightRatio?: number; } export interface IHoverAction { diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index b49cb84951c..0f57de0379a 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -13,7 +13,12 @@ let baseHoverDelegate: IHoverDelegate2 = { setupDelayedHoverAtMouse: () => Disposable.None, hideHover: () => undefined, showAndFocusLastHover: () => undefined, - setupManagedHover: () => null!, + setupManagedHover: () => ({ + dispose: () => undefined, + show: () => undefined, + hide: () => undefined, + update: () => undefined, + }), showManagedHover: () => undefined }; diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index e2608993f70..bb01170201c 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -110,6 +110,7 @@ export interface IPagedListOptions { readonly horizontalScrolling?: boolean; readonly scrollByPage?: boolean; readonly paddingBottom?: number; + readonly alwaysConsumeMouseWheel?: boolean; } function fromPagedListOptions(modelProvider: () => IPagedModel, options: IPagedListOptions): IListOptions { diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index a6c913662f9..b4d6e145634 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -37,6 +37,9 @@ export interface ICheckboxStyles { readonly checkboxBackground: string | undefined; readonly checkboxBorder: string | undefined; readonly checkboxForeground: string | undefined; + readonly checkboxDisabledBackground: string | undefined; + readonly checkboxDisabledForeground: string | undefined; + readonly size?: number; } export const unthemedToggleStyles = { @@ -156,6 +159,10 @@ export class Toggle extends Widget { this._register(this.ignoreGesture(this.domNode)); this.onkeydown(this.domNode, (keyboardEvent) => { + if (!this.enabled) { + return; + } + if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) { this.checked = !this._checked; this._onChange.fire(true); @@ -286,16 +293,28 @@ export class Checkbox extends Widget { enable(): void { this.checkbox.enable(); + this.applyStyles(true); } disable(): void { this.checkbox.disable(); + this.applyStyles(false); } - protected applyStyles(): void { - this.domNode.style.color = this.styles.checkboxForeground || ''; - this.domNode.style.backgroundColor = this.styles.checkboxBackground || ''; - this.domNode.style.borderColor = this.styles.checkboxBorder || ''; + setTitle(newTitle: string): void { + this.checkbox.setTitle(newTitle); + } + + protected applyStyles(enabled = this.enabled): void { + this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || ''; + this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || ''; + this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || ''; + + const size = this.styles.size || 18; + this.domNode.style.width = + this.domNode.style.height = + this.domNode.style.fontSize = `${size}px`; + this.domNode.style.fontSize = `${size - 2}px`; } } diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 9f20f7080ab..3263e2d9922 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -494,6 +494,25 @@ export class Color { return new Color(new RGBA(r, g, b, a)); } + /** + * Mixes the current color with the provided color based on the given factor. + * @param color The color to mix with + * @param factor The factor of mixing (0 means this color, 1 means the input color, 0.5 means equal mix) + * @returns A new color representing the mix + */ + mix(color: Color, factor: number = 0.5): Color { + const normalize = Math.min(Math.max(factor, 0), 1); + const thisRGBA = this.rgba; + const otherRGBA = color.rgba; + + const r = thisRGBA.r + (otherRGBA.r - thisRGBA.r) * normalize; + const g = thisRGBA.g + (otherRGBA.g - thisRGBA.g) * normalize; + const b = thisRGBA.b + (otherRGBA.b - thisRGBA.b) * normalize; + const a = thisRGBA.a + (otherRGBA.a - thisRGBA.a) * normalize; + + return new Color(new RGBA(r, g, b, a)); + } + makeOpaque(opaqueBackground: Color): Color { if (this.isOpaque() || opaqueBackground.rgba.a !== 1) { // only allow to blend onto a non-opaque color onto a opaque color diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index c83402b47c5..9ea80910eeb 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -26,5 +26,6 @@ export const enum MarshalledId { LanguageModelToolResult, LanguageModelTextPart, LanguageModelPromptTsxPart, - LanguageModelDataPart + LanguageModelDataPart, + LanguageModelExtraDataPart, } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 0d2efd3e0a0..ff80ac8bd2d 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -333,6 +333,7 @@ export interface IDefaultChatAgent { readonly publicCodeMatchesUrl: string; readonly manageSettingsUrl: string; readonly managePlanUrl: string; + readonly manageOverageUrl: string; readonly upgradePlanUrl: string; readonly providerId: string; diff --git a/src/vs/base/common/sseParser.ts b/src/vs/base/common/sseParser.ts new file mode 100644 index 00000000000..0990e0247d9 --- /dev/null +++ b/src/vs/base/common/sseParser.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parser for Server-Sent Events (SSE) streams according to the HTML specification. + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + */ + +/** + * Represents an event dispatched from an SSE stream. + */ +export interface ISSEEvent { + /** + * The event type. If not specified, the type is "message". + */ + type: string; + + /** + * The event data. + */ + data: string; + + /** + * The last event ID, used for reconnection. + */ + id?: string; + + /** + * Reconnection time in milliseconds. + */ + retry?: number; +} + +/** + * Callback function type for event dispatch. + */ +export type SSEEventHandler = (event: ISSEEvent) => void; + +const enum Chr { + CR = 13, // '\r' + LF = 10, // '\n' + COLON = 58, // ':' + SPACE = 32, // ' ' +} + +/** + * Parser for Server-Sent Events (SSE) streams. + */ +export class SSEParser { + private dataBuffer = ''; + private eventTypeBuffer = ''; + private currentEventId?: string; + private lastEventIdBuffer?: string; + private reconnectionTime?: number; + private buffer: Uint8Array[] = []; + private endedOnCR = false; + private readonly onEventHandler: SSEEventHandler; + private readonly decoder: TextDecoder; + /** + * Creates a new SSE parser. + * @param onEvent The callback to invoke when an event is dispatched. + */ + constructor(onEvent: SSEEventHandler) { + this.onEventHandler = onEvent; + this.decoder = new TextDecoder('utf-8'); + } + + /** + * Gets the last event ID received by this parser. + */ + public getLastEventId(): string | undefined { + return this.lastEventIdBuffer; + } + /** + * Gets the reconnection time in milliseconds, if one was specified by the server. + */ + public getReconnectionTime(): number | undefined { + return this.reconnectionTime; + } + + /** + * Feeds a chunk of the SSE stream to the parser. + * @param chunk The chunk to parse as a Uint8Array of UTF-8 encoded data. + */ + public feed(chunk: Uint8Array): void { + if (chunk.length === 0) { + return; + } + + let offset = 0; + + // If the data stream was bifurcated between a CR and LF, avoid processing the CR as an extra newline + if (this.endedOnCR && chunk[0] === Chr.LF) { + offset++; + } + this.endedOnCR = false; + + // Process complete lines from the buffer + while (offset < chunk.length) { + const indexCR = chunk.indexOf(Chr.CR, offset); + const indexLF = chunk.indexOf(Chr.LF, offset); + const index = indexCR === -1 ? indexLF : (indexLF === -1 ? indexCR : Math.min(indexCR, indexLF)); + if (index === -1) { + break; + } + + let str = ''; + for (const buf of this.buffer) { + str += this.decoder.decode(buf, { stream: true }); + } + str += this.decoder.decode(chunk.subarray(offset, index)); + this.processLine(str); + + this.buffer.length = 0; + offset = index + (chunk[index] === Chr.CR && chunk[index + 1] === Chr.LF ? 2 : 1); + } + + + if (offset < chunk.length) { + this.buffer.push(chunk.subarray(offset)); + } else { + this.endedOnCR = chunk[chunk.length - 1] === Chr.CR; + } + } + /** + * Processes a single line from the SSE stream. + */ + private processLine(line: string): void { + if (!line.length) { + this.dispatchEvent(); + return; + } + + if (line.startsWith(':')) { + return; + } + + // Parse the field name and value + let field: string; + let value: string; + + const colonIndex = line.indexOf(':'); + if (colonIndex === -1) { + // Line with no colon - the entire line is the field name, value is empty + field = line; + value = ''; + } else { + // Line with a colon - split into field name and value + field = line.substring(0, colonIndex); + value = line.substring(colonIndex + 1); + + // If value starts with a space, remove it + if (value.startsWith(' ')) { + value = value.substring(1); + } + } + + this.processField(field, value); + } + /** + * Processes a field with the given name and value. + */ + private processField(field: string, value: string): void { + switch (field) { + case 'event': + this.eventTypeBuffer = value; + break; + + case 'data': + // Append the value to the data buffer, followed by a newline + this.dataBuffer += value; + this.dataBuffer += '\n'; + break; + + case 'id': + // If the field value doesn't contain NULL, set the last event ID buffer + if (!value.includes('\0')) { + this.currentEventId = this.lastEventIdBuffer = value; + } else { + this.currentEventId = undefined; + } + break; + + case 'retry': + // If the field value consists only of ASCII digits, set the reconnection time + if (/^\d+$/.test(value)) { + this.reconnectionTime = parseInt(value, 10); + } + break; + + // Ignore any other fields + } + } + /** + * Dispatches the event based on the current buffer states. + */ + private dispatchEvent(): void { + // If the data buffer is empty, reset the buffers and return + if (this.dataBuffer === '') { + this.dataBuffer = ''; + this.eventTypeBuffer = ''; + return; + } + + // If the data buffer's last character is a newline, remove it + if (this.dataBuffer.endsWith('\n')) { + this.dataBuffer = this.dataBuffer.substring(0, this.dataBuffer.length - 1); + } + + // Create and dispatch the event + const event: ISSEEvent = { + type: this.eventTypeBuffer || 'message', + data: this.dataBuffer, + }; + + // Add optional fields if they exist + if (this.currentEventId !== undefined) { + event.id = this.currentEventId; + } + + if (this.reconnectionTime !== undefined) { + event.retry = this.reconnectionTime; + } + + // Dispatch the event + this.onEventHandler(event); + + // Reset the data and event type buffers + this.reset(); + } + + /** + * Resets the parser state. + */ + public reset(): void { + this.dataBuffer = ''; + this.eventTypeBuffer = ''; + this.currentEventId = undefined; + // Note: lastEventIdBuffer is not reset as it's used for reconnection + } +} + + diff --git a/src/vs/base/test/common/sseParser.test.ts b/src/vs/base/test/common/sseParser.test.ts new file mode 100644 index 00000000000..c87664b6fb1 --- /dev/null +++ b/src/vs/base/test/common/sseParser.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ISSEEvent, SSEParser } from '../../common/sseParser.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; + +// Helper function to convert string to Uint8Array for testing +function toUint8Array(str: string): Uint8Array { + return new TextEncoder().encode(str); +} + +suite('SSEParser', () => { + let receivedEvents: ISSEEvent[]; + let parser: SSEParser; + + ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + receivedEvents = []; + parser = new SSEParser((event) => receivedEvents.push(event)); + }); + test('handles basic events', () => { + parser.feed(toUint8Array('data: hello world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'message'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('handles events with multiple data fields', () => { + parser.feed(toUint8Array('data: first line\ndata: second line\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'first line\nsecond line'); + }); + test('handles events with explicit event type', () => { + parser.feed(toUint8Array('event: custom\ndata: hello world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('handles events with explicit event type (CRLF)', () => { + parser.feed(toUint8Array('event: custom\r\ndata: hello world\r\n\r\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello world'); + }); + test('stream processing chunks', () => { + for (const lf of ['\n', '\r\n', '\r']) { + const message = toUint8Array(`event: custom${lf}data: hello world${lf}${lf}event: custom2${lf}data: hello world2${lf}${lf}`); + for (let chunkSize = 1; chunkSize < 5; chunkSize++) { + receivedEvents.length = 0; + + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.slice(i, i + chunkSize); + parser.feed(chunk); + } + + assert.deepStrictEqual(receivedEvents, [ + { type: 'custom', data: 'hello world' }, + { type: 'custom2', data: 'hello world2' } + ], `Failed for chunk size ${chunkSize} and line ending ${JSON.stringify(lf)}`); + } + } + }); + test('handles events with ID', () => { + parser.feed(toUint8Array('event: custom\ndata: hello\nid: 123\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].id, '123'); + assert.strictEqual(parser.getLastEventId(), '123'); + }); + + test('ignores comments', () => { + parser.feed(toUint8Array('event: custom\n:this is a comment\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles retry field', () => { + parser.feed(toUint8Array('retry: 5000\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].retry, 5000); + assert.strictEqual(parser.getReconnectionTime(), 5000); + }); + test('handles invalid retry field', () => { + parser.feed(toUint8Array('retry: invalid\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].retry, undefined); + assert.strictEqual(parser.getReconnectionTime(), undefined); + }); + + test('ignores fields with NULL character in ID', () => { + parser.feed(toUint8Array('id: 12\0 3\ndata: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].id, undefined); + assert.strictEqual(parser.getLastEventId(), undefined); + }); + + test('handles fields with no value', () => { + parser.feed(toUint8Array('data\nid\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, ''); + assert.strictEqual(receivedEvents[0].id, ''); + }); + test('handles fields with space after colon', () => { + parser.feed(toUint8Array('data: hello\nevent: custom\nid: 123\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].id, '123'); + }); + + test('handles different line endings (LF)', () => { + parser.feed(toUint8Array('data: hello\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles different line endings (CR)', () => { + parser.feed(toUint8Array('data: hello\r\r')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('handles different line endings (CRLF)', () => { + parser.feed(toUint8Array('data: hello\r\n\r\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + test('handles empty data with blank line', () => { + parser.feed(toUint8Array('data:\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, ''); + }); + + test('ignores events with no data after blank line', () => { + parser.feed(toUint8Array('event: custom\n\n')); + + assert.strictEqual(receivedEvents.length, 0); + }); + + test('supports chunked data', () => { + parser.feed(toUint8Array('event: cus')); + parser.feed(toUint8Array('tom\nda')); + parser.feed(toUint8Array('ta: hello\n')); + parser.feed(toUint8Array('\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].type, 'custom'); + assert.strictEqual(receivedEvents[0].data, 'hello'); + }); + + test('supports spec example', () => { + // Example from the spec + parser.feed(toUint8Array(':This is a comment\ndata: first event\nid: 1\n\n')); + parser.feed(toUint8Array('data:second event\nid\n\n')); + parser.feed(toUint8Array('data: third event\n\n')); + + assert.strictEqual(receivedEvents.length, 3); + assert.strictEqual(receivedEvents[0].data, 'first event'); + assert.strictEqual(receivedEvents[0].id, '1'); + assert.strictEqual(receivedEvents[1].data, 'second event'); + assert.strictEqual(receivedEvents[1].id, ''); + assert.strictEqual(receivedEvents[2].data, ' third event'); + }); + + test('resets correctly', () => { + parser.feed(toUint8Array('data: hello\n')); + parser.reset(); + parser.feed(toUint8Array('data: world\n\n')); + + assert.strictEqual(receivedEvents.length, 1); + assert.strictEqual(receivedEvents[0].data, 'world'); + }); +}); diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css index 5ca36c59620..00170ceefc6 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css @@ -13,7 +13,7 @@ white-space: pre-wrap; } -.monaco-editor .native-edit-context-textarea { +.monaco-editor .ime-text-area { min-width: 0; min-height: 0; margin: 0; diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index df510b25679..05e980bee57 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -31,6 +31,7 @@ import { IAccessibilityService } from '../../../../../platform/accessibility/com import { NativeEditContextRegistry } from './nativeEditContextRegistry.js'; import { IEditorAriaOptions } from '../../../editorBrowser.js'; import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js'; +import { IME } from '../../../../../base/common/ime.js'; // Corresponds to classes in nativeEditContext.css enum CompositionClassName { @@ -51,6 +52,7 @@ export class NativeEditContext extends AbstractEditContext { // Text area used to handle paste events public readonly domNode: FastDomNode; + private readonly _imeTextArea: FastDomNode; private readonly _editContext: EditContext; private readonly _screenReaderSupport: ScreenReaderSupport; private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1); @@ -73,7 +75,7 @@ export class NativeEditContext extends AbstractEditContext { ownerID: string, context: ViewContext, overflowGuardContainer: FastDomNode, - viewController: ViewController, + private readonly _viewController: ViewController, private readonly _visibleRangeProvider: IVisibleRangeProvider, @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @@ -82,6 +84,9 @@ export class NativeEditContext extends AbstractEditContext { this.domNode = new FastDomNode(document.createElement('div')); this.domNode.setClassName(`native-edit-context`); + this._imeTextArea = new FastDomNode(document.createElement('textarea')); + this._imeTextArea.setClassName(`ime-text-area`); + this._imeTextArea.setAttribute('readonly', 'true'); this.domNode.setAttribute('autocorrect', 'off'); this.domNode.setAttribute('autocapitalize', 'off'); this.domNode.setAttribute('autocomplete', 'off'); @@ -90,12 +95,13 @@ export class NativeEditContext extends AbstractEditContext { this._updateDomAttributes(); overflowGuardContainer.appendChild(this.domNode); + overflowGuardContainer.appendChild(this._imeTextArea); this._parent = overflowGuardContainer.domNode; this._selectionChangeListener = this._register(new MutableDisposable()); this._focusTracker = this._register(new FocusTracker(this.domNode.domNode, (newFocusValue: boolean) => { if (newFocusValue) { - this._selectionChangeListener.value = this._setSelectionChangeListener(viewController); + this._selectionChangeListener.value = this._setSelectionChangeListener(this._viewController); this._screenReaderSupport.setIgnoreSelectionChangeTime('onFocus'); } else { this._selectionChangeListener.value = undefined; @@ -115,23 +121,16 @@ export class NativeEditContext extends AbstractEditContext { // result in a `selectionchange` event which we want to ignore this._screenReaderSupport.setIgnoreSelectionChangeTime('onCut'); this._ensureClipboardGetsEditorSelection(e); - viewController.cut(); + this._viewController.cut(); })); - this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => viewController.emitKeyUp(new StandardKeyboardEvent(e)))); - this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => { - - const standardKeyboardEvent = new StandardKeyboardEvent(e); - - // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further - if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { - standardKeyboardEvent.stopPropagation(); - } - viewController.emitKeyDown(standardKeyboardEvent); - })); + this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e))); + this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e))); + this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e))); + this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') { - this._onType(viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); + this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } })); this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => { @@ -154,7 +153,7 @@ export class NativeEditContext extends AbstractEditContext { multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null; mode = metadata.mode; } - viewController.paste(text, pasteOnNewLine, multicursorText, mode); + this._viewController.paste(text, pasteOnNewLine, multicursorText, mode); })); // Edit context events @@ -178,26 +177,39 @@ export class NativeEditContext extends AbstractEditContext { updateRangeEnd: e.updateRangeEnd - 1 }; highSurrogateCharacter = undefined; - this._emitTypeEvent(viewController, textUpdateEvent); + this._emitTypeEvent(this._viewController, textUpdateEvent); return; } } - this._emitTypeEvent(viewController, e); + this._emitTypeEvent(this._viewController, e); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => { // Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors - viewController.compositionStart(); + this._viewController.compositionStart(); // Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionStart(); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => { // Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state // Updates the composition state of the cursor controller which determines behavior of typing with interceptors - viewController.compositionEnd(); + this._viewController.compositionEnd(); // Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers this._context.viewModel.onCompositionEnd(); })); + let reenableTracking: boolean = false; + this._register(IME.onDidChange(() => { + if (IME.enabled && reenableTracking) { + this.domNode.focus(); + this._focusTracker.resume(); + reenableTracking = false; + } + if (!IME.enabled && this.isFocused()) { + this._focusTracker.pause(); + this._imeTextArea.focus(); + reenableTracking = true; + } + })); this._register(NativeEditContextRegistry.register(ownerID, this)); } @@ -207,6 +219,7 @@ export class NativeEditContext extends AbstractEditContext { // Force blue the dom node so can write in pane with no native edit context after disposal this.domNode.domNode.blur(); this.domNode.domNode.remove(); + this._imeTextArea.domNode.remove(); super.dispose(); } @@ -314,6 +327,19 @@ export class NativeEditContext extends AbstractEditContext { // --- Private methods --- + private _onKeyUp(e: KeyboardEvent) { + this._viewController.emitKeyUp(new StandardKeyboardEvent(e)); + } + + private _onKeyDown(e: KeyboardEvent) { + const standardKeyboardEvent = new StandardKeyboardEvent(e); + // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further + if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { + standardKeyboardEvent.stopPropagation(); + } + this._viewController.emitKeyDown(standardKeyboardEvent); + } + private _updateDomAttributes(): void { const options = this._context.configuration.options; this.domNode.domNode.setAttribute('tabindex', String(options.get(EditorOption.tabIndex))); @@ -450,20 +476,19 @@ export class NativeEditContext extends AbstractEditContext { return; } const options = this._context.configuration.options; - const lineHeight = options.get(EditorOption.lineHeight); const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; const parentBounds = this._parent.getBoundingClientRect(); - const modelStartPosition = this._primarySelection.getStartPosition(); - const viewStartPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelStartPosition); - const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewStartPosition.lineNumber); + const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); + const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber); const top = parentBounds.top + verticalOffsetStart - this._scrollTop; - const height = (this._primarySelection.endLineNumber - this._primarySelection.startLineNumber + 1) * lineHeight; + const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); + const height = verticalOffsetEnd - verticalOffsetStart; let left = parentBounds.left + contentLeft - this._scrollLeft; let width: number; if (this._primarySelection.isEmpty()) { - const linesVisibleRanges = ctx.visibleRangeForPosition(viewStartPosition); + const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition()); if (linesVisibleRanges) { left += linesVisibleRanges.left; } @@ -483,7 +508,6 @@ export class NativeEditContext extends AbstractEditContext { } const options = this._context.configuration.options; const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const lineHeight = options.get(EditorOption.lineHeight); const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; const parentBounds = this._parent.getBoundingClientRect(); @@ -497,7 +521,8 @@ export class NativeEditContext extends AbstractEditContext { const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition); const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange); const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? []; - const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(characterViewRange.startLineNumber); + const lineNumber = characterViewRange.startLineNumber; + const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber); const top = parentBounds.top + characterVerticalOffset - this._scrollTop; let left = 0; @@ -509,6 +534,7 @@ export class NativeEditContext extends AbstractEditContext { break; } } + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber); characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight)); } this._editContext.updateCharacterBounds(e.rangeStart, characterBounds); @@ -548,7 +574,7 @@ export class NativeEditContext extends AbstractEditContext { let previousSelectionChangeEventTime = 0; return addDisposableListener(this.domNode.domNode.ownerDocument, 'selectionchange', () => { const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); - if (!this.isFocused() || !isScreenReaderOptimized) { + if (!this.isFocused() || !isScreenReaderOptimized || !IME.enabled) { return; } const screenReaderContentState = this._screenReaderSupport.screenReaderContentState; diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts index faa0ec2f2cc..90f3e9839be 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts @@ -15,6 +15,7 @@ export interface ITypeData { export class FocusTracker extends Disposable { private _isFocused: boolean = false; + private _isPaused: boolean = false; constructor( private readonly _domNode: HTMLElement, @@ -22,16 +23,31 @@ export class FocusTracker extends Disposable { ) { super(); this._register(addDisposableListener(this._domNode, 'focus', () => { + if (this._isPaused) { + return; + } // Here we don't trust the browser and instead we check // that the active element is the one we are tracking // (this happens when cmd+tab is used to switch apps) this.refreshFocusState(); })); this._register(addDisposableListener(this._domNode, 'blur', () => { + if (this._isPaused) { + return; + } this._handleFocusedChanged(false); })); } + public pause(): void { + this._isPaused = true; + } + + public resume(): void { + this._isPaused = false; + this.refreshFocusState(); + } + private _handleFocusedChanged(focused: boolean): void { if (this._isFocused === focused) { return; diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index 40c319cf813..dad5348efa9 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -28,7 +28,6 @@ export class ScreenReaderSupport { private _contentWidth: number = 1; private _contentHeight: number = 1; private _divWidth: number = 1; - private _lineHeight: number = 1; private _fontInfo!: FontInfo; private _accessibilityPageSize: number = 1; private _ignoreSelectionChangeTime: number = 0; @@ -75,7 +74,6 @@ export class ScreenReaderSupport { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); this._divWidth = Math.round(wrappingColumn * this._fontInfo.typicalHalfwidthCharacterWidth); } @@ -133,10 +131,13 @@ export class ScreenReaderSupport { return; } - const offsetForStartPositionWithinEditor = this._context.viewLayout.getVerticalOffsetForLineNumber(this._screenReaderContentState.startPositionWithinEditor.lineNumber); - const offsetForPositionLineNumber = this._context.viewLayout.getVerticalOffsetForLineNumber(positionLineNumber); - const scrollTop = offsetForPositionLineNumber - offsetForStartPositionWithinEditor; - this._doRender(scrollTop, top, this._contentLeft, this._divWidth, this._lineHeight); + // The
where we render the screen reader content does not support variable line heights, + // all the lines must have the same height. We use the line height of the cursor position as the + // line height for all lines. + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(positionLineNumber); + const lineNumberWithinStateAboveCursor = positionLineNumber - this._screenReaderContentState.startPositionWithinEditor.lineNumber; + const scrollTop = lineNumberWithinStateAboveCursor * lineHeight; + this._doRender(scrollTop, top, this._contentLeft, this._divWidth, lineHeight); } private _renderAtTopLeft(): void { @@ -151,6 +152,7 @@ export class ScreenReaderSupport { this._domNode.setLeft(left); this._domNode.setWidth(width); this._domNode.setHeight(height); + this._domNode.setLineHeight(height); this._domNode.domNode.scrollTop = scrollTop; } diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index a3f8b75544b..11a1f09ca04 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -125,7 +125,6 @@ export class TextAreaEditContext extends AbstractEditContext { private _contentWidth: number; private _contentHeight: number; private _fontInfo: FontInfo; - private _lineHeight: number; private _emptySelectionClipboard: boolean; private _copyWithSyntaxHighlighting: boolean; @@ -169,7 +168,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); @@ -591,7 +589,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); - this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off'); @@ -745,6 +742,8 @@ export class TextAreaEditContext extends AbstractEditContext { } // Try to render the textarea with the color/font style to match the text under it + const viewPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(startPosition.lineNumber, 1)); + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber); const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1); const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1); @@ -753,7 +752,7 @@ export class TextAreaEditContext extends AbstractEditContext { (textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null) ); - this.textArea.domNode.scrollTop = lineCount * this._lineHeight; + this.textArea.domNode.scrollTop = lineCount * lineHeight; this.textArea.domNode.scrollLeft = scrollLeft; this._doRender({ @@ -761,7 +760,7 @@ export class TextAreaEditContext extends AbstractEditContext { top: top, left: left, width: width, - height: this._lineHeight, + height: lineHeight, useCover: false, color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground], italic: presentation.italic, @@ -798,19 +797,21 @@ export class TextAreaEditContext extends AbstractEditContext { if (platform.isMacintosh || this._accessibilitySupport === AccessibilitySupport.Enabled) { // For the popup emoji input, we will make the text area as high as the line height // We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers + const lineNumber = this._primaryCursorPosition.lineNumber; + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber); this._doRender({ lastRenderPosition: this._primaryCursorPosition, top, left: this._textAreaWrapping ? this._contentLeft : left, width: this._textAreaWidth, - height: this._lineHeight, + height: lineHeight, useCover: false }); // In case the textarea contains a word, we're going to try to align the textarea's cursor // with our cursor by scrolling the textarea as much as possible this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left; const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? newlinecount(this.textArea.domNode.value.substring(0, this.textArea.domNode.selectionStart)); - this.textArea.domNode.scrollTop = lineCount * this._lineHeight; + this.textArea.domNode.scrollTop = lineCount * lineHeight; return; } @@ -848,6 +849,7 @@ export class TextAreaEditContext extends AbstractEditContext { ta.setLeft(renderData.left); ta.setWidth(renderData.width); ta.setHeight(renderData.height); + ta.setLineHeight(renderData.height); ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : ''); ta.setFontStyle(renderData.italic ? 'italic' : ''); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 2694f30368f..8f0ed8dabb8 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -19,7 +19,7 @@ import { IDiffComputationResult, ILineChange } from '../common/diff/legacyLinesD import * as editorCommon from '../common/editorCommon.js'; import { GlyphMarginLane, ICursorStateComputer, IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from '../common/model.js'; import { InjectedText } from '../common/modelLineProjectionData.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from '../common/textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelLineHeightChangedEvent } from '../common/textModelEvents.js'; import { IEditorWhitespace, IViewModel } from '../common/viewModel.js'; import { OverviewRulerZone } from '../common/viewModel/overviewZoneManager.js'; import { MenuId } from '../../platform/actions/common/actions.js'; @@ -891,6 +891,13 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getConfiguredWordAtPosition(position: Position): IWordAtPosition | null; + /** + * An event emitted when line heights from decorations change + * @internal + * @event + */ + onDidChangeLineHeight: Event; + /** * Get value of the current model attached to this editor. * @see {@link ITextModel.getValue} @@ -1068,6 +1075,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the line height for the line number. + */ + getLineHeightForLineNumber(lineNumber: number): number; + /** * Set the model ranges that will be hidden in the view. * Hidden areas are stored per source. diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 295d5bf1ca1..39a0bce8add 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -460,6 +460,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { public afterContentClassName: string | undefined; public glyphMarginClassName: string | undefined; public isWholeLine: boolean; + public lineHeight?: number; public overviewRuler: IModelDecorationOverviewRulerOptions | undefined; public stickiness: TrackedRangeStickiness | undefined; public beforeInjectedText: InjectedTextOptions | undefined; @@ -520,6 +521,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { const options = providerArgs.options; this.isWholeLine = Boolean(options.isWholeLine); + this.lineHeight = options.lineHeight; this.stickiness = options.rangeBehavior; const lightOverviewRulerColor = options.light && options.light.overviewRulerColor || options.overviewRulerColor; @@ -549,6 +551,7 @@ class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider { className: this.className, glyphMarginClassName: this.glyphMarginClassName, isWholeLine: this.isWholeLine, + lineHeight: this.lineHeight, overviewRuler: this.overviewRuler, stickiness: this.stickiness, before: this.beforeInjectedText, diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index c33296a34e6..9635e03c818 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -61,6 +61,7 @@ export class HoverWidget extends Widget implements IHoverWidget { private _isLocked: boolean = false; private _enableFocusTraps: boolean = false; private _addedFocusTrap: boolean = false; + private _maxHeightRatioRelativeToWindow: number = 0.5; private get _targetWindow(): Window { return dom.getWindow(this._target.targetElements[0]); @@ -127,6 +128,11 @@ export class HoverWidget extends Widget implements IHoverWidget { this._enableFocusTraps = true; } + const maxHeightRatio = options.appearance?.maxHeightRatio; + if (maxHeightRatio !== undefined && maxHeightRatio > 0 && maxHeightRatio <= 1) { + this._maxHeightRatioRelativeToWindow = maxHeightRatio; + } + // Default to position above when the position is unspecified or a mouse event this._hoverPosition = options.position?.hoverPosition === undefined ? HoverPosition.ABOVE @@ -551,7 +557,7 @@ export class HoverWidget extends Widget implements IHoverWidget { } private adjustHoverMaxHeight(target: TargetRect): void { - let maxHeight = this._targetWindow.innerHeight / 2; + let maxHeight = this._targetWindow.innerHeight * this._maxHeightRatioRelativeToWindow; // When force position is enabled, restrict max height if (this._forcePosition) { diff --git a/src/vs/editor/browser/view/renderingContext.ts b/src/vs/editor/browser/view/renderingContext.ts index e5ec4b30eca..6fe17c1a9f7 100644 --- a/src/vs/editor/browser/view/renderingContext.ts +++ b/src/vs/editor/browser/view/renderingContext.ts @@ -61,6 +61,10 @@ export abstract class RestrictedRenderingContext { return this._viewLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._viewLayout.getLineHeightForLineNumber(lineNumber); + } + public getDecorationsInViewport(): ViewModelDecoration[] { return this.viewportData.getDecorationsInViewport(); } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 4058205be2f..1b64760a7f4 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -10,6 +10,7 @@ import { EditorOption } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import * as viewEvents from '../../common/viewEvents.js'; import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import { ViewContext } from '../../common/viewModel/viewContext.js'; /** * Represents a visible line @@ -255,7 +256,8 @@ export class VisibleLinesCollection { private readonly _linesCollection: RenderedLinesCollection = new RenderedLinesCollection(this._lineFactory); constructor( - private readonly _lineFactory: ILineFactory + private readonly _viewContext: ViewContext, + private readonly _lineFactory: ILineFactory, ) { } @@ -354,7 +356,7 @@ export class VisibleLinesCollection { const inp = this._linesCollection._get(); - const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData); + const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData, this._viewContext); const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, @@ -383,6 +385,7 @@ class ViewLayerRenderer { private readonly _domNode: HTMLElement, private readonly _lineFactory: ILineFactory, private readonly _viewportData: ViewportData, + private readonly _viewContext: ViewContext ) { } @@ -467,7 +470,7 @@ class ViewLayerRenderer { for (let i = startIndex; i <= endIndex; i++) { const lineNumber = rendLineNumberStart + i; - lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._viewportData.lineHeight); + lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._lineHeightForLineNumber(lineNumber)); } } @@ -571,7 +574,8 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this._viewportData.lineHeight, this._viewportData, sb); + const renderedLineNumber = i + rendLineNumberStart; + const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -601,7 +605,8 @@ class ViewLayerRenderer { continue; } - const renderResult = line.renderLine(i + rendLineNumberStart, deltaTop[i], this._viewportData.lineHeight, this._viewportData, sb); + const renderedLineNumber = i + rendLineNumberStart; + const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb); if (!renderResult) { // line does not need rendering continue; @@ -616,4 +621,8 @@ class ViewLayerRenderer { } } } + + private _lineHeightForLineNumber(lineNumber: number): number { + return this._viewContext.viewLayout.getLineHeightForLineNumber(lineNumber); + } } diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 6b8e10f341c..1f351bb3571 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -24,7 +24,7 @@ export class ViewOverlays extends ViewPart { constructor(context: ViewContext) { super(context); - this._visibleLines = new VisibleLinesCollection({ + this._visibleLines = new VisibleLinesCollection(this._context, { createLine: () => new ViewOverlayLine(this._dynamicOverlays) }); this.domNode = this._visibleLines.domNode; @@ -178,6 +178,8 @@ export class ViewOverlayLine implements IVisibleLine { sb.appendString(String(deltaTop)); sb.appendString('px;height:'); sb.appendString(String(lineHeight)); + sb.appendString('px;line-height:'); + sb.appendString(String(lineHeight)); sb.appendString('px;">'); sb.appendString(result); sb.appendString('
'); @@ -189,6 +191,7 @@ export class ViewOverlayLine implements IVisibleLine { if (this._domNode) { this._domNode.setTop(deltaTop); this._domNode.setHeight(lineHeight); + this._domNode.setLineHeight(lineHeight); } } } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 6ae696f90dd..f3e89362b99 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -198,7 +198,6 @@ class Widget { private readonly _fixedOverflowWidgets: boolean; private _contentWidth: number; private _contentLeft: number; - private _lineHeight: number; private _primaryAnchor: PositionPair = new PositionPair(null, null); private _secondaryAnchor: PositionPair = new PositionPair(null, null); @@ -227,7 +226,6 @@ class Widget { this._fixedOverflowWidgets = options.get(EditorOption.fixedOverflowWidgets); this._contentWidth = layoutInfo.contentWidth; this._contentLeft = layoutInfo.contentLeft; - this._lineHeight = options.get(EditorOption.lineHeight); this._affinity = null; this._preference = []; @@ -246,7 +244,6 @@ class Widget { public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void { const options = this._context.configuration.options; - this._lineHeight = options.get(EditorOption.lineHeight); if (e.hasChanged(EditorOption.layoutInfo)) { const layoutInfo = options.get(EditorOption.layoutInfo); this._contentLeft = layoutInfo.contentLeft; @@ -403,12 +400,12 @@ class Widget { * The content widget should touch if possible the secondary anchor. */ private _getAnchorsCoordinates(ctx: RenderingContext): { primary: AnchorCoordinate | null; secondary: AnchorCoordinate | null } { - const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity, this._lineHeight); + const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity); const secondaryViewPosition = (this._secondaryAnchor.viewPosition?.lineNumber === this._primaryAnchor.viewPosition?.lineNumber ? this._secondaryAnchor.viewPosition : null); - const secondary = getCoordinates(secondaryViewPosition, this._affinity, this._lineHeight); + const secondary = getCoordinates(secondaryViewPosition, this._affinity); return { primary, secondary }; - function getCoordinates(position: Position | null, affinity: PositionAffinity | null, lineHeight: number): AnchorCoordinate | null { + function getCoordinates(position: Position | null, affinity: PositionAffinity | null): AnchorCoordinate | null { if (!position) { return null; } @@ -421,6 +418,7 @@ class Widget { // Left-align widgets that should appear :before content const left = (position.column === 1 && affinity === PositionAffinity.LeftOfInjectedText ? 0 : horizontalPosition.left); const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop; + const lineHeight = ctx.getLineHeightForLineNumber(position.lineNumber); return new AnchorCoordinate(top, left, lineHeight); } } diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index c1234141862..dd565eac9e4 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -415,7 +415,8 @@ export class GlyphMarginWidgets extends ViewPart { // Render decorations, reusing previous dom nodes as possible for (let i = 0; i < this._decorationGlyphsToRender.length; i++) { const dec = this._decorationGlyphsToRender[i]; - const top = ctx.viewportData.relativeVerticalOffset[dec.lineNumber - ctx.viewportData.startLineNumber]; + const decLineNumber = dec.lineNumber; + const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber]; const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight; let domNode: FastDomNode; @@ -426,13 +427,14 @@ export class GlyphMarginWidgets extends ViewPart { this._managedDomNodes.push(domNode); this.domNode.appendChild(domNode); } + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber); domNode.setClassName(`cgmr codicon ` + dec.combinedClassName); domNode.setPosition(`absolute`); domNode.setTop(top); domNode.setLeft(left); domNode.setWidth(width); - domNode.setHeight(this._lineHeight); + domNode.setHeight(lineHeight); } // remove extra dom nodes diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index b4380373c52..1e4164e9f81 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -47,7 +47,6 @@ export class ViewCursor { private _cursorStyle: TextEditorCursorStyle; private _lineCursorWidth: number; - private _lineHeight: number; private _typicalHalfwidthCharacterWidth: number; private _isVisible: boolean; @@ -64,7 +63,6 @@ export class ViewCursor { const fontInfo = options.get(EditorOption.fontInfo); this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); @@ -73,7 +71,7 @@ export class ViewCursor { // Create the dom node this._domNode = createFastDomNode(document.createElement('div')); this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); - this._domNode.setHeight(this._lineHeight); + this._domNode.setHeight(this._context.viewLayout.getLineHeightForLineNumber(1)); this._domNode.setTop(0); this._domNode.setLeft(0); applyFontInfo(this._domNode, fontInfo); @@ -131,7 +129,6 @@ export class ViewCursor { const fontInfo = options.get(EditorOption.fontInfo); this._cursorStyle = options.get(EditorOption.effectiveCursorStyle); - this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth); applyFontInfo(this._domNode, fontInfo); @@ -193,7 +190,8 @@ export class ViewCursor { } const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.bigNumbersDelta; - return new ViewCursorRenderData(top, left, paddingLeft, width, this._lineHeight, textContent, textContentClassName); + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(position.lineNumber); + return new ViewCursorRenderData(top, left, paddingLeft, width, lineHeight, textContent, textContentClassName); } const visibleRangeForCharacter = ctx.linesVisibleRangesForRange(new Range(position.lineNumber, position.column, position.lineNumber, position.column + nextGrapheme.length), false); @@ -223,11 +221,12 @@ export class ViewCursor { } let top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.bigNumbersDelta; - let height = this._lineHeight; + const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(position.lineNumber); + let height = lineHeight; // Underline might interfere with clicking if (this._cursorStyle === TextEditorCursorStyle.Underline || this._cursorStyle === TextEditorCursorStyle.UnderlineThin) { - top += this._lineHeight - 2; + top += lineHeight - 2; height = 2; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 16229cd928a..476bdeeb68c 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -175,6 +175,8 @@ export class ViewLine implements IVisibleLine { sb.appendString(String(deltaTop)); sb.appendString('px;height:'); sb.appendString(String(lineHeight)); + sb.appendString('px;line-height:'); + sb.appendString(String(lineHeight)); sb.appendString('px;" class="'); sb.appendString(ViewLine.CLASS_NAME); sb.appendString('">'); @@ -211,6 +213,7 @@ export class ViewLine implements IVisibleLine { if (this._renderedViewLine && this._renderedViewLine.domNode) { this._renderedViewLine.domNode.setTop(deltaTop); this._renderedViewLine.domNode.setHeight(lineHeight); + this._renderedViewLine.domNode.setLineHeight(lineHeight); } } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index caebbb73701..140b62be89e 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -145,7 +145,7 @@ export class ViewLines extends ViewPart implements IViewLines { this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = new VisibleLinesCollection({ + this._visibleLines = new VisibleLinesCollection(this._context, { createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions), }); this.domNode = this._visibleLines.domNode; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 56cc4692dc0..d26aba26a26 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -146,7 +146,7 @@ export class WhitespaceOverlay extends DynamicViewOverlay { const fauxIndentLength = lineData.minColumn - 1; const onlyBoundary = (this._options.renderWhitespace === 'boundary'); const onlyTrailing = (this._options.renderWhitespace === 'trailing'); - const lineHeight = this._options.lineHeight; + const lineHeight = ctx.getLineHeightForLineNumber(lineNumber); const middotWidth = this._options.middotWidth; const wsmiddotWidth = this._options.wsmiddotWidth; const spaceWidth = this._options.spaceWidth; diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 2a87a577f9f..f2a45afad39 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -44,7 +44,7 @@ import { EndOfLinePreference, IAttachedView, ICursorStateComputer, IIdentifiedSi import { ClassName } from '../../../common/model/intervalTree.js'; import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from '../../../common/textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelLineHeightChangedEvent } from '../../../common/textModelEvents.js'; import { VerticalRevealType } from '../../../common/viewEvents.js'; import { IEditorWhitespace, IViewModel } from '../../../common/viewModel.js'; import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; @@ -91,6 +91,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidChangeModelDecorations: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelDecorations: Event = this._onDidChangeModelDecorations.event; + private readonly _onDidChangeLineHeight: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); + public readonly onDidChangeLineHeight: Event = this._onDidChangeLineHeight.event; + private readonly _onDidChangeModelTokens: Emitter = this._register(new Emitter({ deliveryQueue: this._deliveryQueue })); public readonly onDidChangeModelTokens: Event = this._onDidChangeModelTokens.event; @@ -594,6 +597,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + if (!this._modelData) { + return -1; + } + const viewPosition = this._modelData.viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)); + return this._modelData.viewModel.viewLayout.getLineHeightForLineNumber(viewPosition.lineNumber); + } + public setHiddenAreas(ranges: IRange[], source?: unknown, forceUpdate?: boolean): void { this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source, forceUpdate); } @@ -1598,11 +1609,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const top = CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, position.lineNumber, position.column) - this.getScrollTop(); const left = this._modelData.view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.glyphMarginWidth + layoutInfo.lineNumbersWidth + layoutInfo.decorationsWidth - this.getScrollLeft(); - + const height = this.getLineHeightForLineNumber(position.lineNumber); return { top: top, left: left, - height: options.get(EditorOption.lineHeight) + height }; } @@ -1776,6 +1787,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE case OutgoingViewModelEventKind.ModelTokensChanged: this._onDidChangeModelTokens.fire(e.event); break; + case OutgoingViewModelEventKind.ModelLineHeightChanged: + this._onDidChangeLineHeight.fire(e.event); + break; } })); diff --git a/src/vs/editor/common/codecs/frontMatterCodec/constants.ts b/src/vs/editor/common/codecs/frontMatterCodec/constants.ts new file mode 100644 index 00000000000..0df65a7b88d --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/constants.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NewLine } from '../linesCodec/tokens/newLine.js'; +import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; +import { FormFeed, Space, Tab, VerticalTab } from '../simpleCodec/tokens/index.js'; + +/** + * List of valid "space" tokens that are valid between + * different entities of the Front Matter header. + */ +export const VALID_SPACE_TOKENS = Object.freeze([ + Space, Tab, CarriageReturn, NewLine, FormFeed, VerticalTab, +]); diff --git a/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts b/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts new file mode 100644 index 00000000000..02db7f7fca7 --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/frontMatterDecoder.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VALID_SPACE_TOKENS } from './constants.js'; +import { Word } from '../simpleCodec/tokens/index.js'; +import { TokenStream } from '../utils/tokenStream.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { ReadableStream } from '../../../../base/common/stream.js'; +import { FrontMatterToken, FrontMatterRecord } from './tokens/index.js'; +import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; +import { SimpleDecoder, type TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; +import { PartialFrontMatterRecord, PartialFrontMatterRecordName, PartialFrontMatterRecordNameWithDelimiter } from './parsers/frontMatterRecord.js'; + +/** + * Tokens produced by this decoder. + */ +export type TFrontMatterToken = FrontMatterRecord | TSimpleDecoderToken; + +/** + * Decoder capable of parsing Front Matter contents from a sequence of simple tokens. + */ +export class FrontMatterDecoder extends BaseDecoder { + /** + * Current parser reference responsible for parsing a specific sequence + * of tokens into a standalone token. + */ + private current?: PartialFrontMatterRecordName | PartialFrontMatterRecordNameWithDelimiter | PartialFrontMatterRecord; + + constructor( + stream: ReadableStream | TokenStream, + ) { + if (stream instanceof TokenStream) { + super(stream); + + return; + } + + super(new SimpleDecoder(stream)); + } + + protected override onStreamData(token: TSimpleDecoderToken): void { + if (this.current !== undefined) { + const acceptResult = this.current.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.reEmitCurrentTokens(); + + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + delete this.current; + return; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterToken) { + this._onData.fire(nextParser); + + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + delete this.current; + return; + } + + this.current = nextParser; + if (wasTokenConsumed === false) { + this._onData.fire(token); + } + + return; + } + + // a word token starts a new record + if (token instanceof Word) { + this.current = new PartialFrontMatterRecordName(token); + return; + } + + // re-emit all "space" tokens immediately as all of them + // are valid while we are not in the "record parsing" mode + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this._onData.fire(token); + return; + } + } + + // unexpected token type, re-emit existing tokens and continue + this.reEmitCurrentTokens(); + } + + protected override onStreamEnd(): void { + try { + if (this.current === undefined) { + return; + } + + this.reEmitCurrentTokens(); + } finally { + delete this.current; + super.onStreamEnd(); + } + } + + /** + * Re-emit tokens accumulated so far in the current parser object. + */ + protected reEmitCurrentTokens(): void { + if (this.current === undefined) { + return; + } + + for (const token of this.current.tokens) { + this._onData.fire(token); + } + delete this.current; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts new file mode 100644 index 00000000000..22b54f4517f --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterArray.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VALID_SPACE_TOKENS } from '../constants.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { FrontMatterArray } from '../tokens/frontMatterArray.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Comma, LeftBracket, RightBracket } from '../../simpleCodec/tokens/index.js'; +import { PartialFrontMatterValue, VALID_VALUE_START_TOKENS } from './frontMatterValue.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * List of tokens that can go in-between array items + * and array brackets. + */ +const VALID_DELIMITER_TOKENS = Object.freeze([ + ...VALID_SPACE_TOKENS, + Comma, +]); + +/** + * Responsible for parsing an array syntax (or "inline sequence" + * in YAML terms), e.g. `[1, '2', true, 2.54]` + */ +export class PartialFrontMatterArray extends ParserBase { + /** + * Current parser reference responsible for parsing an array "value". + */ + private currentValueParser?: PartialFrontMatterValue; + + /** + * Whether an array item is allowed in the current position + * of the token sequence. E.g., items are allowed after + * a command or a open bracket, but not immediately after + * another item in the array. + */ + private arrayItemAllowed = true; + + constructor( + private readonly startToken: LeftBracket, + ) { + /** + * Sanity check - logic inside the {@link PartialFrontMatterArray.accept accept} method + * above assumes that the {@link VALID_DELIMITER_TOKENS} tokens list does not intersect + * with the {@link VALID_VALUE_START_TOKENS} tokens list. + * + * Note! the `as` type casting below is ok since we offload the type intersection check + * to the runtime, and is required to avoid compilation errors in Typescript. + */ + for (const DelimiterToken of VALID_DELIMITER_TOKENS) { + for (const ValueStartToken of VALID_VALUE_START_TOKENS as unknown[]) { + assert( + DelimiterToken !== ValueStartToken, + `Delimiter tokens list must not contain value start token '${ValueStartToken}'.`, + ); + } + } + + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + this.currentTokens.push(nextParser); + delete this.currentValueParser; + + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + if (token instanceof RightBracket) { + // sanity check in case this block moves around + // to a different place in the code + assert( + this.currentValueParser === undefined, + `Unexpected end of array. Last value is not finished.`, + ); + + this.currentTokens.push(token); + + this.isConsumed = true; + return { + result: 'success', + nextParser: this.asArrayToken(), + wasTokenConsumed: true, + }; + } + + // iterate until a valid value start token is found + for (const ValidToken of VALID_DELIMITER_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + if ((this.arrayItemAllowed === false) && token instanceof Comma) { + this.arrayItemAllowed = true; + } + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // once we found a valid start value token, create a new value parser + if ((this.arrayItemAllowed === true) && PartialFrontMatterValue.isValueStartToken(token)) { + this.currentValueParser = new PartialFrontMatterValue(); + this.arrayItemAllowed = false; + + return this.accept(token); + } + + // in all other cases fail because of the unexpected token type + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + /** + * Convert current parser into a {@link FrontMatterArray} token, + * if possible. + * + * @throws if the last token in the accumulated token list + * is not a closing bracket ({@link RightBracket}). + */ + public asArrayToken(): FrontMatterArray { + this.isConsumed = true; + const endToken = this.currentTokens[this.currentTokens.length - 1]; + + assertDefined( + endToken, + `No tokens found.`, + ); + + assert( + endToken instanceof RightBracket, + 'Cannot find a closing bracket of the array.', + ); + + const valueTokens: FrontMatterValueToken[] = []; + for (const currentToken of this.currentTokens) { + if (currentToken instanceof FrontMatterValueToken) { + valueTokens.push(currentToken); + } + } + + return new FrontMatterArray([ + this.startToken, + ...valueTokens, + endToken, + ]); + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts new file mode 100644 index 00000000000..6af49acbbce --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterRecord.ts @@ -0,0 +1,277 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../base/common/assert.js'; +import { PartialFrontMatterValue } from './frontMatterValue.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Colon, Word, Dash, Space, Tab } from '../../simpleCodec/tokens/index.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; +import { FrontMatterValueToken, FrontMatterRecordName, type TRecordNameToken, type TRecordSpaceToken, FrontMatterRecordDelimiter, FrontMatterRecord } from '../tokens/index.js'; + +/** + * Tokens that can be used inside a record name. + */ +const VALID_NAME_TOKENS = [ + Word, Dash, +]; + +/** + * List of a "space" tokens that are allowed in between + * record name, delimiter and value tokens inside a record. + * + * E.g. the following is a valid record with `\t` used as a "space" token: + * + * ``` + * \t\tname\t\t:\t\t'value'\t\t\n + * ``` + */ +const VALID_SPACE_TOKENS = [ + Space, Tab, +]; + +/** + * List of tokens that terminate a record name. + */ +const VALID_NAME_STOP_TOKENS = [ + ...VALID_SPACE_TOKENS, + Colon, +]; + +/** + * Parser for a `name` part of a Front Matter record. + * + * E.g., `'name'` in the example below: + * + * ``` + * name: 'value' + * ``` + */ +export class PartialFrontMatterRecordName extends ParserBase { + constructor( + startToken: Word, + ) { + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + for (const ValidToken of VALID_NAME_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // once name is followed by a "space" token or a "colon", we have the full + // record name hence can transition to the next parser + for (const SpaceOrDelimiterToken of VALID_NAME_STOP_TOKENS) { + if (token instanceof SpaceOrDelimiterToken) { + const recordName = new FrontMatterRecordName(this.currentTokens); + + this.isConsumed = true; + return { + result: 'success', + nextParser: new PartialFrontMatterRecordNameWithDelimiter([recordName, token]), + wasTokenConsumed: true, + }; + } + } + + // in all other cases fail due to the unexpected token type for a record name + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * Parser for a record `name` with the `: ` delimiter. + * + * * E.g., `name:` in the example below: + * + * ``` + * name: 'value' + * ``` + */ +export class PartialFrontMatterRecordNameWithDelimiter extends ParserBase { + constructor( + tokens: readonly [FrontMatterRecordName, TRecordSpaceToken | Colon], + ) { + super([...tokens]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + const previousToken = this.currentTokens[this.currentTokens.length - 1]; + + const isSpacingToken = (token instanceof Space) || (token instanceof Tab); + + // delimiter must always be a `:` followed by a "space" character + // once we encounter that sequence, we can transition to the next parser + if ((isSpacingToken === true) && (previousToken instanceof Colon)) { + const recordDelimiter = new FrontMatterRecordDelimiter([ + previousToken, + token, + ]); + + const recordName = this.currentTokens[0]; + + // sanity check + assert( + recordName instanceof FrontMatterRecordName, + `Expected a front matter record name, got '${recordName}'.`, + ); + + this.isConsumed = true; + return { + result: 'success', + nextParser: new PartialFrontMatterRecord( + [recordName, recordDelimiter], + ), + wasTokenConsumed: true, + }; + } + + // allow some spacing before the colon delimiter + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // include the colon delimiter + if (token instanceof Colon) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // otherwise fail due to the unexpected token type between + // record name and record name delimiter tokens + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * Parser for a `record` inside a Front Matter header. + * + * * E.g., `name: 'value'` in the example below: + * + * ``` + * --- + * name: 'value' + * isExample: true + * --- + * ``` + */ +export class PartialFrontMatterRecord extends ParserBase { + constructor( + tokens: [FrontMatterRecordName, FrontMatterRecordDelimiter], + ) { + super(tokens); + } + + /** + * Current parser reference responsible for parsing the "value" part of the record. + */ + private currentValueParser?: PartialFrontMatterValue; + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'failure') { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + this.currentTokens.push(nextParser); + delete this.currentValueParser; + + this.isConsumed = true; + try { + return { + result: 'success', + nextParser: FrontMatterRecord.fromTokens([ + this.currentTokens[0], + this.currentTokens[1], + nextParser, + ]), + wasTokenConsumed, + }; + } catch (_error) { + return { + result: 'failure', + wasTokenConsumed, + }; + } + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + // iterate until the first "value" token is found + for (const ValidToken of VALID_SPACE_TOKENS) { + if (token instanceof ValidToken) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + } + + // if token can start a "value" sequence, parse the value + if (PartialFrontMatterValue.isValueStartToken(token)) { + this.currentValueParser = new PartialFrontMatterValue(); + + return this.accept(token); + } + + // otherwise fail due to the unexpected token type for a record value + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts new file mode 100644 index 00000000000..349c26d28cc --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterString.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../base/common/assert.js'; +import { SimpleToken } from '../../simpleCodec/tokens/index.js'; +import { assertDefined } from '../../../../../base/common/types.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { FrontMatterString, TQuoteToken } from '../tokens/frontMatterString.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * Parser responsible for parsing a string value. + */ +export class PartialFrontMatterString extends ParserBase> { + constructor( + private readonly startToken: TQuoteToken, + ) { + super([startToken]); + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult> { + this.currentTokens.push(token); + + // iterate until a `matching end quote` is found + if ((token instanceof SimpleToken) && (this.startToken.sameType(token))) { + return { + result: 'success', + nextParser: this.asStringToken(), + wasTokenConsumed: true, + }; + } + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Convert the current parser into a {@link FrontMatterString} token, + * if possible. + * + * @throws if the first and last tokens are not quote tokens of the same type. + */ + public asStringToken(): FrontMatterString { + const endToken = this.currentTokens[this.currentTokens.length - 1]; + + assertDefined( + endToken, + `No matching end token found.`, + ); + + assert( + this.startToken.sameType(endToken), + `String starts with \`${this.startToken.text}\`, but ends with \`${endToken.text}\`.`, + ); + + return new FrontMatterString([ + this.startToken, + ...this.currentTokens + .slice(1, this.currentTokens.length - 1), + endToken, + ]); + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts new file mode 100644 index 00000000000..8e1e32d032a --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/parsers/frontMatterValue.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { PartialFrontMatterArray } from './frontMatterArray.js'; +import { PartialFrontMatterString } from './frontMatterString.js'; +import { FrontMatterBoolean } from '../tokens/frontMatterBoolean.js'; +import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; +import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; +import { Word, Quote, DoubleQuote, LeftBracket } from '../../simpleCodec/tokens/index.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * List of tokens that can start a "value" sequence. + * + * - {@link Word} - can be a `boolean` value + * - {@link Quote}, {@link DoubleQuote} - can start a `string` value + * - {@link LeftBracket} - can start an `array` value + */ +export const VALID_VALUE_START_TOKENS = Object.freeze([ + Word, + Quote, + DoubleQuote, + LeftBracket, +]); + +/** + * Type alias for a token that can start a "value" sequence. + */ +type TValueStartToken = InstanceType; + +/** + * Parser responsible for parsing a "value" sequence in a Front Matter header. + */ +export class PartialFrontMatterValue extends ParserBase { + /** + * Current parser reference responsible for parsing + * a specific "value" sequence. + */ + private currentValueParser?: PartialFrontMatterString | PartialFrontMatterArray; + + /** + * Get the tokens that were accumulated so far. + */ + public override get tokens(): readonly TSimpleDecoderToken[] { + if (this.currentValueParser === undefined) { + return []; + } + + return this.currentValueParser.tokens; + } + + @assertNotConsumed + public accept(token: TSimpleDecoderToken): TAcceptTokenResult { + if (this.currentValueParser !== undefined) { + const acceptResult = this.currentValueParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + // current value parser is consumed with its child value parser + this.isConsumed = this.currentValueParser.consumed; + + if (result === 'success') { + const { nextParser } = acceptResult; + + if (nextParser instanceof FrontMatterValueToken) { + return { + result: 'success', + nextParser, + wasTokenConsumed, + }; + } + + this.currentValueParser = nextParser; + return { + result: 'success', + nextParser: this, + wasTokenConsumed, + }; + } + + return { + result: 'failure', + wasTokenConsumed, + }; + } + + // if the first token represents a `quote` character, try to parse a string value + if ((token instanceof Quote) || (token instanceof DoubleQuote)) { + this.currentValueParser = new PartialFrontMatterString(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // if the first token represents a `[` character, try to parse an array value + if (token instanceof LeftBracket) { + this.currentValueParser = new PartialFrontMatterArray(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + // if the first token represents a `word` try to parse a boolean + if (token instanceof Word) { + // in either success or failure case, the parser is consumed + this.isConsumed = true; + + try { + return { + result: 'success', + nextParser: FrontMatterBoolean.fromToken(token), + wasTokenConsumed: true, + }; + } catch (_error) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + } + + // in all other cases fail due to unexpected value sequence + this.isConsumed = true; + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + /** + * Check if provided token can be a start of a "value" sequence. + * See {@link VALID_VALUE_START_TOKENS} for the list of valid tokens. + */ + public static isValueStartToken( + token: BaseToken, + ): token is TValueStartToken { + for (const ValidToken of VALID_VALUE_START_TOKENS) { + if (token instanceof ValidToken) { + return true; + } + } + + return false; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts new file mode 100644 index 00000000000..b9e54253bcd --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterArray.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { FrontMatterValueToken, TValueTypeName } from './frontMatterToken.js'; +import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/index.js'; + +/** + * Token that represents an `array` value in a Front Matter header. + */ +export class FrontMatterArray extends FrontMatterValueToken<'array'> { + /** + * Name of the `array` value type. + */ + public override readonly valueTypeName = 'array'; + + constructor( + /** + * List of tokens of the array value. Must start and end + * with square brackets, but tokens in the middle hold + * only the value tokens, omitting commas and spaces. + */ + public readonly tokens: readonly [ + LeftBracket, + ...FrontMatterValueToken[], + RightBracket, + ], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + /** + * List of the array items. + */ + public get items(): readonly FrontMatterValueToken[] { + const result = []; + + for (const token of this.tokens) { + if (token instanceof FrontMatterValueToken) { + result.push(token); + } + } + + return result; + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + public override toString(): string { + return `front-matter-array(${this.shortText()})${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts new file mode 100644 index 00000000000..0203d3c9ab0 --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterBoolean.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { Word } from '../../simpleCodec/tokens/index.js'; +import { FrontMatterValueToken } from './frontMatterToken.js'; +import { assertDefined } from '../../../../../base/common/types.js'; + +/** + * Token that represents a `boolean` value in a Front Matter header. + */ +export class FrontMatterBoolean extends FrontMatterValueToken<'boolean'> { + /** + * Name of the `boolean` value type. + */ + public override readonly valueTypeName = 'boolean'; + + constructor( + range: Range, + public readonly value: boolean, + ) { + super(range); + } + + public static fromToken(token: Word): FrontMatterBoolean { + const value = asBoolean(token); + + assertDefined( + value, + `Cannot convert '${token}' to a boolean value.`, + ); + + return new FrontMatterBoolean(token.range, value); + } + + public override get text(): string { + return `${this.value}`; + } + + public override toString(): string { + return `front-matter-boolean(${this.shortText()})${this.range}`; + } +} + +/** + * Try to convert a {@link Word} token to a `boolean` value. + */ +const asBoolean = ( + token: Word, +): boolean | null => { + if (token.text.toLowerCase() === 'true') { + return true; + } + + if (token.text.toLowerCase() === 'false') { + return false; + } + + return null; +}; diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts new file mode 100644 index 00000000000..96ecdeabe43 --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterRecord.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { Colon, Word, Dash, Space, Tab } from '../../simpleCodec/tokens/index.js'; +import { FrontMatterToken, FrontMatterValueToken, TValueTypeName } from '../tokens/frontMatterToken.js'; + +/** + * Type for tokens that can be used inside a record name. + */ +export type TNameToken = Word | Dash; + +/** + * Type for tokens that can be used as "space" in-between record + * name, delimiter and value. + */ +export type TSpaceToken = Space | Tab; + +/** + * Token representing a `record name` inside a Front Matter record. + * + * E.g., `name` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecordName extends FrontMatterToken { + constructor( + public readonly tokens: readonly TNameToken[], + ) { + super(BaseToken.fullRange(tokens)); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-record-name(${this.shortText()})${this.range}`; + } +} + +/** + * Token representing a delimiter of a record inside a Front Matter header. + * + * E.g., `: ` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecordDelimiter extends FrontMatterToken { + constructor( + public readonly tokens: readonly [Colon, TSpaceToken], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-delimiter(${this.shortText()})${this.range}`; + } +} + +/** + * Token representing a `record` inside a Front Matter header. + * + * E.g., `name: 'value'` in the example below: + * + * ``` + * --- + * name: 'value' + * --- + * ``` + */ +export class FrontMatterRecord extends FrontMatterToken { + constructor( + private readonly tokens: readonly [FrontMatterRecordName, FrontMatterRecordDelimiter, FrontMatterValueToken], + ) { + super( + BaseToken.fullRange(tokens), + ); + } + + /** + * Token that represent `name` of the record. + * + * E.g., `tools` in the example below: + * + * ``` + * --- + * tools: ['value'] + * --- + * ``` + */ + public get nameToken(): FrontMatterRecordName { + return this.tokens[0]; + } + + /** + * Token that represent `value` of the record. + * + * E.g., `['value']` in the example below: + * + * ``` + * --- + * tools: ['value'] + * --- + * ``` + */ + public get valueToken(): FrontMatterValueToken { + return this.tokens[2]; + } + + /** + * Create new instance from a list of tokens. + * + * @throws if: + * - the list of tokens is not exactly 3 tokens long + * - the first token in the list is not a `FrontMatterRecordName` + * - the second token in the list is not a `FrontMatterRecordDelimiter` + * - the third token in the list is not a `FrontMatterValueToken` + * + */ + public static fromTokens( + tokens: readonly FrontMatterToken[], + ): FrontMatterRecord { + assert( + tokens.length === 3, + `A front matter record must consist of exactly 3 tokens, got '${tokens.length}'.`, + ); + + const token1 = tokens[0]; + const token2 = tokens[1]; + const token3 = tokens[2]; + + assert( + token1 instanceof FrontMatterRecordName, + `Token #1 must be a front matter record name, got '${token1}'.`, + ); + assert( + token2 instanceof FrontMatterRecordDelimiter, + `Token #2 must be a front matter record delimiter, got '${token2}'.`, + ); + assert( + token3 instanceof FrontMatterValueToken, + `Token #3 must be a front matter value, got '${token3}'.`, + ); + + return new FrontMatterRecord([ + token1, token2, token3, + ]); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-record(${this.shortText()})${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts new file mode 100644 index 00000000000..abd368a721d --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterString.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { FrontMatterValueToken } from './frontMatterToken.js'; +import { Quote, DoubleQuote } from '../../simpleCodec/tokens/index.js'; + +/** + * Type for any quote token that can be used to wrap a string. + */ +export type TQuoteToken = Quote | DoubleQuote; + +/** + * Token that represents a string value in a Front Matter header. + */ +export class FrontMatterString extends FrontMatterValueToken<'string'> { + /** + * Name of the `string` value type. + */ + public override readonly valueTypeName = 'string'; + + constructor( + public readonly tokens: readonly [TQuote, ...BaseToken[], TQuote], + ) { + super(BaseToken.fullRange(tokens)); + } + + /** + * Text of the string value without the wrapping quotes. + */ + public get cleanText(): string { + return BaseToken.render( + this.tokens.slice(1, this.tokens.length - 1), + ); + } + + public override get text(): string { + return BaseToken.render(this.tokens); + } + + public override toString(): string { + return `front-matter-string(${this.shortText()})${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.ts new file mode 100644 index 00000000000..ce9eb28225c --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/frontMatterToken.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; + +/** + * Base class for all tokens inside a Front Matter header. + */ +export abstract class FrontMatterToken extends BaseToken { } + +/** + * List of all currently supported value types. + */ +export type TValueTypeName = 'string' | 'boolean' | 'array'; + +/** + * Base class for all tokens that represent a `value` inside a Front Matter header. + */ +export abstract class FrontMatterValueToken extends FrontMatterToken { + /** + * Type name of the `value` represented by this token. + */ + public abstract readonly valueTypeName: TTypeName; +} diff --git a/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts b/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts new file mode 100644 index 00000000000..a9959b44726 --- /dev/null +++ b/src/vs/editor/common/codecs/frontMatterCodec/tokens/index.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { FrontMatterArray } from './frontMatterArray.js'; +export { FrontMatterString } from './frontMatterString.js'; +export { FrontMatterBoolean } from './frontMatterBoolean.js'; +export { FrontMatterToken, FrontMatterValueToken } from './frontMatterToken.js'; +export { + FrontMatterRecordName, + FrontMatterRecordDelimiter, + FrontMatterRecord, + type TNameToken as TRecordNameToken, + type TSpaceToken as TRecordSpaceToken, +} from './frontMatterRecord.js'; diff --git a/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts index 469a66cd703..5ab1d06d5c9 100644 --- a/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts +++ b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts @@ -26,10 +26,10 @@ const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLin * * The parsing process starts with single `[` token and collects all tokens until * the first `]` token is encountered. In this successful case, the parser transitions - * into the {@linkcode MarkdownLinkCaption} parser type which continues the general + * into the {@link MarkdownLinkCaption} parser type which continues the general * parsing process of the markdown link. * - * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * Otherwise, if one of the stop characters defined in the {@link MARKDOWN_LINK_STOP_CHARACTERS} * is encountered before the `]` token, the parsing process is aborted which is communicated to * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no @@ -70,7 +70,7 @@ export class PartialMarkdownLinkCaption extends ParserBase { /** * Number of open parenthesis in the sequence. - * See comment in the {@linkcode accept} method for more details. + * See comment in the {@link accept} method for more details. */ private openParensCount: number = 1; diff --git a/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts b/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts index 378d451d641..e4466e0ee13 100644 --- a/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts +++ b/src/vs/editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.ts @@ -86,12 +86,11 @@ export class MarkdownExtensionsDecoder extends BaseDecoder { */ protected isConsumed: boolean = false; + /** + * Whether the parser object was "consumed" hence must not be used anymore. + */ + public get consumed(): boolean { + return this.isConsumed; + } + /** * Number of tokens at the initialization of the current parser. */ diff --git a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts index e9d97747c3c..6847f9e5671 100644 --- a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts +++ b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts @@ -19,8 +19,11 @@ import { Colon, Slash, Space, + Quote, + Comma, FormFeed, DollarSign, + DoubleQuote, VerticalTab, type TBracket, LeftBracket, @@ -42,9 +45,9 @@ import { ISimpleTokenClass, SimpleToken } from './tokens/simpleToken.js'; /** * Type for all simple tokens. */ -export type TSimpleToken = Space | Tab | VerticalTab | At +export type TSimpleToken = Space | Tab | VerticalTab | At | Quote | DoubleQuote | CarriageReturn | NewLine | FormFeed | TBracket | TAngleBracket | TCurlyBrace - | TParenthesis | Colon | Hash | Dash | ExclamationMark | Slash | DollarSign + | TParenthesis | Colon | Hash | Dash | ExclamationMark | Slash | DollarSign | Comma | TLineBreakToken; /** @@ -58,9 +61,9 @@ export type TSimpleDecoderToken = TSimpleToken | Word; * an arbitrary "text" sequence and is emitted as a single {@link Word} token. */ export const WELL_KNOWN_TOKENS: readonly ISimpleTokenClass[] = Object.freeze([ - LeftParenthesis, RightParenthesis, LeftBracket, RightBracket, - LeftCurlyBrace, RightCurlyBrace, LeftAngleBracket, RightAngleBracket, - Space, Tab, VerticalTab, FormFeed, Colon, Hash, Dash, ExclamationMark, At, Slash, DollarSign, + LeftParenthesis, RightParenthesis, LeftBracket, RightBracket, LeftCurlyBrace, RightCurlyBrace, + LeftAngleBracket, RightAngleBracket, Space, Tab, VerticalTab, FormFeed, Colon, Hash, Dash, + ExclamationMark, At, Slash, DollarSign, Quote, DoubleQuote, Comma, ]); /** diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts new file mode 100644 index 00000000000..c76ff95b096 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/comma.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `,` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Comma extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: ',' = ','; + + /** + * Return text representation of the token. + */ + public override get text() { + return Comma.symbol; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `comma${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts new file mode 100644 index 00000000000..5fc5b4595e8 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/doubleQuote.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `"` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class DoubleQuote extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '"' = '"'; + + /** + * Return text representation of the token. + */ + public override get text() { + return DoubleQuote.symbol; + } + + /** + * Checks if the provided token is of the same type + * as the current one. + */ + public sameType(other: BaseToken): other is typeof this { + return (other instanceof this.constructor); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `double-quote${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts index dd0a4c8bf36..b8e38bc3325 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/index.ts @@ -9,12 +9,15 @@ export { Dash } from './dash.js'; export { Hash } from './hash.js'; export { Word } from './word.js'; export { Colon } from './colon.js'; +export { Quote } from './quote.js'; export { Slash } from './slash.js'; export { Space } from './space.js'; +export { Comma } from './comma.js'; export { FormFeed } from './formFeed.js'; export { DollarSign } from './dollarSign.js'; export { VerticalTab } from './verticalTab.js'; export { SimpleToken } from './simpleToken.js'; +export { DoubleQuote } from './doubleQuote.js'; export { ExclamationMark } from './exclamationMark.js'; export { type TBracket, LeftBracket, RightBracket } from './brackets.js'; export { type TCurlyBrace, LeftCurlyBrace, RightCurlyBrace } from './curlyBraces.js'; diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts new file mode 100644 index 00000000000..85c331b0be8 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/quote.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { SimpleToken } from './simpleToken.js'; + +/** + * A token that represent a `'` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Quote extends SimpleToken { + /** + * The underlying symbol of the token. + */ + public static override readonly symbol: '\'' = '\''; + + /** + * Return text representation of the token. + */ + public override get text() { + return Quote.symbol; + } + + /** + * Checks if the provided token is of the same type + * as the current one. + */ + public sameType(other: BaseToken): other is Quote { + return (other instanceof this.constructor); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `quote${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/utils/tokenStream.ts b/src/vs/editor/common/codecs/utils/tokenStream.ts new file mode 100644 index 00000000000..d5d0c5f0967 --- /dev/null +++ b/src/vs/editor/common/codecs/utils/tokenStream.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../baseToken.js'; +import { assert, assertNever } from '../../../../base/common/assert.js'; +import { ObservableDisposable } from '../../../../base/common/observableDisposable.js'; +import { newWriteableStream, WriteableStream, ReadableStream } from '../../../../base/common/stream.js'; + +/** + * A readable stream of provided tokens. + */ +export class TokenStream extends ObservableDisposable implements ReadableStream { + /** + * Underlying writable stream instance. + */ + private readonly stream: WriteableStream; + + /** + * Index of the next token to be sent. + */ + private index: number; + + /** + * Interval reference that is used to periodically send + * tokens to the stream in the background. + */ + private interval: ReturnType | undefined; + + /** + * Number of tokens left to be sent. + */ + private get tokensLeft(): number { + return this.tokens.length - this.index; + } + + constructor( + private readonly tokens: readonly T[], + ) { + super(); + + this.stream = newWriteableStream(null); + this.index = 0; + + // send couple of tokens immediately + this.sendTokens(); + } + + /** + * Start periodically sending tokens to the stream + * asynchronously in the background. + */ + public startStream(): this { + // already running, noop + if (this.interval !== undefined) { + return this; + } + + // no tokens to send, end the stream immediately + if (this.tokens.length === 0) { + this.stream.end(); + return this; + } + + // periodically send tokens to the stream + this.interval = setInterval(() => { + if (this.tokensLeft === 0) { + clearInterval(this.interval); + delete this.interval; + + return; + } + + this.sendTokens(); + }, 1); + + return this; + } + + /** + * Stop tokens sending interval. + */ + public stopStream(): this { + if (this.interval === undefined) { + return this; + } + + clearInterval(this.interval); + delete this.interval; + + return this; + } + + /** + * Sends a provided number of tokens to the stream. + */ + private sendTokens( + tokensCount: number = 25, + ): void { + if (this.tokensLeft <= 0) { + return; + } + + // send up to 10 tokens at a time + let tokensToSend = Math.min(this.tokensLeft, tokensCount); + while (tokensToSend > 0) { + assert( + this.index < this.tokens.length, + `Token index '${this.index}' is out of bounds.`, + ); + + this.stream.write(this.tokens[this.index]); + this.index++; + tokensToSend--; + } + + // if sent all tokens, end the stream immediately + if (this.tokensLeft === 0) { + this.stream.end(); + } + } + + public pause(): void { + this.stopStream(); + + return this.stream.pause(); + } + + public resume(): void { + this.startStream(); + + return this.stream.resume(); + } + + public destroy(): void { + this.dispose(); + } + + public removeListener(event: string, callback: Function): void { + return this.stream.removeListener(event, callback); + } + + public on(event: 'data', callback: (data: T) => void): void; + public on(event: 'error', callback: (err: Error) => void): void; + public on(event: 'end', callback: () => void): void; + public on(event: 'data' | 'error' | 'end', callback: (arg?: any) => void): void { + if (event === 'data') { + this.stream.on(event, callback); + // this is the convention of the readable stream, - when + // the `data` event is registered, the stream is started + this.startStream(); + + return; + } + + if (event === 'error') { + return this.stream.on(event, callback); + } + + if (event === 'end') { + return this.stream.on(event, callback); + } + + assertNever( + event, + `Unexpected event name '${event}'.`, + ); + } + + /** + * Cleanup send interval and destroy the stream. + */ + public override dispose(): void { + this.stopStream(); + this.stream.destroy(); + + super.dispose(); + } +} diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index e25edb4fb50..9cf2cd59a83 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -634,6 +634,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; fontSize?: string; + lineHeight?: number; textDecoration?: string; cursor?: string; color?: string | ThemeColor; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 146c9830e04..5134e5e4d93 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -19,7 +19,7 @@ import { IWordAtPosition } from './core/wordHelper.js'; import { FormattingOptions } from './languages.js'; import { ILanguageSelection } from './languages/language.js'; import { IBracketPairsTextModelPart } from './textModelBracketPairs.js'; -import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from './textModelEvents.js'; +import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; import { IGuidesTextModelPart } from './textModelGuides.js'; import { ITokenizationTextModelPart } from './tokenizationTextModelPart.js'; import { UndoRedoGroup } from '../../platform/undoRedo/common/undoRedo.js'; @@ -218,6 +218,10 @@ export interface IModelDecorationOptions { * with the specified {@link IModelDecorationGlyphMarginOptions} in the glyph margin. */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; + /** + * If set, the decoration will override the line height of the lines it spans. + */ + lineHeight?: number | null; /** * If set, the decoration will be rendered in the lines decorations with this CSS class name. */ @@ -1108,6 +1112,12 @@ export interface ITextModel { */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** * @internal */ @@ -1238,6 +1248,14 @@ export interface ITextModel { * @event */ readonly onDidChangeDecorations: Event; + /** + * An event emitted when line heights from decorations changes. + * This event is emitted only when adding, removing or changing a decoration + * and not when doing edits in the model (i.e. when decoration ranges change) + * @internal + * @event + */ + readonly onDidChangeLineHeight: Event; /** * An event emitted when the model options have changed. * @event diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index c17724ca9ad..0ea09949e81 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -40,13 +40,14 @@ import { SearchParams, TextModelSearch } from './textModelSearch.js'; import { TokenizationTextModelPart } from './tokenizationTextModelPart.js'; import { AttachedViews } from './tokens.js'; import { IBracketPairsTextModelPart } from '../textModelBracketPairs.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted, ModelLineHeightChangedEvent, ModelLineHeightChanged } from '../textModelEvents.js'; import { IGuidesTextModelPart } from '../textModelGuides.js'; import { ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { IColorTheme } from '../../../platform/theme/common/themeService.js'; import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js'; import { TokenArray } from '../tokens/tokenArray.js'; +import { SetWithKey } from '../../../base/common/collections.js'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -213,7 +214,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onWillDispose: Emitter = this._register(new Emitter()); public readonly onWillDispose: Event = this._onWillDispose.event; - private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter(affectedInjectedTextLines => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines))); + private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter((affectedInjectedTextLines, affectedLineHeights) => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines, affectedLineHeights))); public readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; public get onDidChangeLanguage() { return this._tokenizationTextModelPart.onDidChangeLanguage; } @@ -228,6 +229,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onDidChangeInjectedText: Emitter = this._register(new Emitter()); + private readonly _onDidChangeLineHeight: Emitter = this._register(new Emitter()); + public readonly onDidChangeLineHeight: Event = this._onDidChangeLineHeight.event; + private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter()); public onDidChangeContent(listener: (e: IModelContentChangedEvent) => void): IDisposable { return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.contentChangedEvent)); @@ -413,6 +417,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati || this._onDidChangeOptions.hasListeners() || this._onDidChangeAttached.hasListeners() || this._onDidChangeInjectedText.hasListeners() + || this._onDidChangeLineHeight.hasListeners() || this._eventEmitter.hasListeners() ); } @@ -1576,17 +1581,19 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati //#region Decorations - private handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines: Set | null): void { + private handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines: Set | null, affectedLineHeights: Set | null): void { // This is called before the decoration changed event is fired. - if (affectedInjectedTextLines === null || affectedInjectedTextLines.size === 0) { - return; + if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { + const affectedLines = Array.from(affectedInjectedTextLines); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); + } + if (affectedLineHeights && affectedLineHeights.size > 0) { + const affectedLines = Array.from(affectedLineHeights); + const lineHeightChangeEvent = affectedLines.map(specialLineHeightChange => new ModelLineHeightChanged(specialLineHeightChange.ownerId, specialLineHeightChange.decorationId, specialLineHeightChange.lineNumber, specialLineHeightChange.lineHeight)); + this._onDidChangeLineHeight.fire(new ModelLineHeightChangedEvent(lineHeightChangeEvent)); } - - const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); - - this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); } public changeDecorations(callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T | null { @@ -1606,10 +1613,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._changeDecorationImpl(id, newRange); + this._changeDecorationImpl(ownerId, id, newRange); }, changeDecorationOptions: (id: string, options: model.IModelDecorationOptions) => { - this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); + this._changeDecorationOptionsImpl(ownerId, id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { this._deltaDecorationsImpl(ownerId, [id], []); @@ -1761,6 +1768,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._decorationsTree.getAllInjectedText(this, ownerId); } + public getCustomLineHeightsDecorations(ownerId: number = 0): model.IModelDecoration[] { + return this._decorationsTree.getAllCustomLineHeights(this, ownerId); + } + private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { const startOffset = this._buffer.getOffsetAt(lineNumber, 1); const endOffset = startOffset + this._buffer.getLineLength(lineNumber); @@ -1789,7 +1800,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._buffer.getRangeAt(start, end - start); } - private _changeDecorationImpl(decorationId: string, _range: IRange): void { + private _changeDecorationImpl(ownerId: number, decorationId: string, _range: IRange): void { const node = this._decorations[decorationId]; if (!node) { return; @@ -1803,6 +1814,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const oldRange = this.getDecorationRange(decorationId); this._onDidChangeDecorations.recordLineAffectedByInjectedText(oldRange!.startLineNumber); } + if (node.options.lineHeight !== null) { + const oldRange = this.getDecorationRange(decorationId); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, oldRange!.startLineNumber, null); + } const range = this._validateRangeRelaxedNoAllocations(_range); const startOffset = this._buffer.getOffsetAt(range.startLineNumber, range.startColumn); @@ -1819,9 +1834,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (node.options.before) { this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber); } + if (node.options.lineHeight !== null) { + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, range.startLineNumber, node.options.lineHeight); + } } - private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { + private _changeDecorationOptionsImpl(ownerId: number, decorationId: string, options: ModelDecorationOptions): void { const node = this._decorations[decorationId]; if (!node) { return; @@ -1841,6 +1859,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const nodeRange = this._decorationsTree.getNodeRange(this, node); this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber); } + if (node.options.lineHeight !== null || options.lineHeight !== null) { + const nodeRange = this._decorationsTree.getNodeRange(this, node); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, nodeRange.startLineNumber, options.lineHeight); + } const movedInOverviewRuler = nodeWasInOverviewRuler !== nodeIsInOverviewRuler; const changedWhetherInjectedText = isOptionsInjectedText(options) !== isNodeInjectedText(node); @@ -1871,8 +1893,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (oldDecorationIndex < oldDecorationsLen) { // (1) get ourselves an old node + let decorationId: string; do { - node = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; + decorationId = oldDecorationsIds[oldDecorationIndex++]; + node = this._decorations[decorationId]; } while (!node && oldDecorationIndex < oldDecorationsLen); // (2) remove the node from the tree (if it exists) @@ -1885,7 +1909,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const nodeRange = this._decorationsTree.getNodeRange(this, node); this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber); } - + if (node.options.lineHeight !== null) { + const nodeRange = this._decorationsTree.getNodeRange(this, node); + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, decorationId, nodeRange.startLineNumber, null); + } this._decorationsTree.delete(node); if (!suppressEvents) { @@ -1920,7 +1947,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (node.options.before) { this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber); } - + if (node.options.lineHeight !== null) { + this._onDidChangeDecorations.recordLineAffectedByLineHeightChange(ownerId, node.id, range.startLineNumber, node.options.lineHeight); + } if (!suppressEvents) { this._onDidChangeDecorations.checkAffectedAndFire(options); } @@ -2090,6 +2119,12 @@ class DecorationsTrees { return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } + public getAllCustomLineHeights(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] { + const versionId = host.getVersionId(); + const result = this._search(filterOwnerId, false, false, versionId, false); + return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); + } + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId, onlyMarginDecorations); @@ -2316,6 +2351,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly hoverMessage: IMarkdownString | IMarkdownString[] | null; readonly glyphMarginHoverMessage: IMarkdownString | IMarkdownString[] | null; readonly isWholeLine: boolean; + readonly lineHeight: number | null; readonly showIfCollapsed: boolean; readonly collapseOnReplaceEdit: boolean; readonly overviewRuler: ModelDecorationOverviewRulerOptions | null; @@ -2351,6 +2387,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || null; this.lineNumberHoverMessage = options.lineNumberHoverMessage || null; this.isWholeLine = options.isWholeLine || false; + this.lineHeight = options.lineHeight ?? null; this.showIfCollapsed = options.showIfCollapsed || false; this.collapseOnReplaceEdit = options.collapseOnReplaceEdit || false; this.overviewRuler = options.overviewRuler ? new ModelDecorationOverviewRulerOptions(options.overviewRuler) : null; @@ -2391,6 +2428,20 @@ function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorat return ModelDecorationOptions.createDynamic(options); } +class LineHeightChangingDecoration { + + public static toKey(obj: LineHeightChangingDecoration): string { + return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; + } + + constructor( + public readonly ownerId: number, + public readonly decorationId: string, + public readonly lineNumber: number, + public readonly lineHeight: number | null + ) { } +} + class DidChangeDecorationsEmitter extends Disposable { private readonly _actual: Emitter = this._register(new Emitter()); @@ -2401,10 +2452,11 @@ class DidChangeDecorationsEmitter extends Disposable { private _affectsMinimap: boolean; private _affectsOverviewRuler: boolean; private _affectedInjectedTextLines: Set | null = null; + private _affectedLineHeights: SetWithKey | null = null; private _affectsGlyphMargin: boolean; private _affectsLineNumber: boolean; - constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null) => void) { + constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null, affectedLineHeights: SetWithKey | null) => void) { super(); this._deferredCnt = 0; this._shouldFireDeferred = false; @@ -2431,6 +2483,8 @@ class DidChangeDecorationsEmitter extends Disposable { this._affectedInjectedTextLines?.clear(); this._affectedInjectedTextLines = null; + this._affectedLineHeights?.clear(); + this._affectedLineHeights = null; } } @@ -2441,6 +2495,13 @@ class DidChangeDecorationsEmitter extends Disposable { this._affectedInjectedTextLines.add(lineNumber); } + public recordLineAffectedByLineHeightChange(ownerId: number, decorationId: string, lineNumber: number, lineHeight: number | null): void { + if (!this._affectedLineHeights) { + this._affectedLineHeights = new SetWithKey([], LineHeightChangingDecoration.toKey); + } + this._affectedLineHeights.add(new LineHeightChangingDecoration(ownerId, decorationId, lineNumber, lineHeight)); + } + public checkAffectedAndFire(options: ModelDecorationOptions): void { this._affectsMinimap ||= !!options.minimap?.position; this._affectsOverviewRuler ||= !!options.overviewRuler?.color; @@ -2465,7 +2526,7 @@ class DidChangeDecorationsEmitter extends Disposable { } private doFire() { - this.handleBeforeFire(this._affectedInjectedTextLines); + this.handleBeforeFire(this._affectedInjectedTextLines, this._affectedLineHeights); const event: IModelDecorationsChangedEvent = { affectsMinimap: this._affectsMinimap, diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 593e5194573..f44bee76294 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -988,4 +988,4 @@ export enum WrappingIndent { * DeepIndent => wrapped lines get +2 indentation toward the parent. */ DeepIndent = 3 -} +} \ No newline at end of file diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index c35d0472106..3123b120b28 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -234,6 +234,37 @@ export class ModelRawLineChanged { } } + +/** + * An event describing that a line height has changed in the model. + * @internal + */ +export class ModelLineHeightChanged { + /** + * Editor owner ID + */ + public readonly ownerId: number; + /** + * The decoration ID that has changed. + */ + public readonly decorationId: string; + /** + * The line that has changed. + */ + public readonly lineNumber: number; + /** + * The line height on the line. + */ + public readonly lineHeight: number | null; + + constructor(ownerId: number, decorationId: string, lineNumber: number, lineHeight: number | null) { + this.ownerId = ownerId; + this.decorationId = decorationId; + this.lineNumber = lineNumber; + this.lineHeight = lineHeight; + } +} + /** * An event describing that line(s) have been deleted in a model. * @internal @@ -361,6 +392,19 @@ export class ModelInjectedTextChangedEvent { } } +/** + * An event describing a change of a line height. + * @internal + */ +export class ModelLineHeightChangedEvent { + + public readonly changes: ModelLineHeightChanged[]; + + constructor(changes: ModelLineHeightChanged[]) { + this.changes = changes; + } +} + /** * @internal */ diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts new file mode 100644 index 00000000000..37996f9f967 --- /dev/null +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { binarySearch2 } from '../../../base/common/arrays.js'; +import { intersection } from '../../../base/common/collections.js'; + +export class CustomLine { + + public index: number; + public lineNumber: number; + public specialHeight: number; + public prefixSum: number; + public maximumSpecialHeight: number; + public decorationId: string; + public deleted: boolean; + + constructor(decorationId: string, index: number, lineNumber: number, specialHeight: number, prefixSum: number) { + this.decorationId = decorationId; + this.index = index; + this.lineNumber = lineNumber; + this.specialHeight = specialHeight; + this.prefixSum = prefixSum; + this.maximumSpecialHeight = specialHeight; + this.deleted = false; + } +} + +/** + * Manages line heights in the editor with support for custom line heights from decorations. + * + * This class maintains an ordered collection of line heights, where each line can have either + * the default height or a custom height specified by decorations. It supports efficient querying + * of individual line heights as well as accumulated heights up to a specific line. + * + * Line heights are stored in a sorted array for efficient binary search operations. Each line + * with custom height is represented by a {@link CustomLine} object which tracks its special height, + * accumulated height prefix sum, and associated decoration ID. + * + * The class optimizes performance by: + * - Using binary search to locate lines in the ordered array + * - Batching updates through a pending changes mechanism + * - Computing prefix sums for O(1) accumulated height lookup + * - Tracking maximum height for lines with multiple decorations + * - Efficiently handling document changes (line insertions and deletions) + * + * When lines are inserted or deleted, the manager updates line numbers and prefix sums + * for all affected lines. It also handles special cases like decorations that span + * the insertion/deletion points by re-applying those decorations appropriately. + * + * All query operations automatically commit pending changes to ensure consistent results. + * Clients can modify line heights by adding or removing custom line height decorations, + * which are tracked by their unique decoration IDs. + */ +export class LineHeightsManager { + + private _decorationIDToCustomLine: ArrayMap = new ArrayMap(); + private _orderedCustomLines: CustomLine[] = []; + private _pendingSpecialLinesToInsert: CustomLine[] = []; + private _invalidIndex: number = 0; + private _defaultLineHeight: number; + private _hasPending: boolean = false; + + constructor(defaultLineHeight: number, customLineHeightData: ICustomLineHeightData[]) { + this._defaultLineHeight = defaultLineHeight; + if (customLineHeightData.length > 0) { + for (const data of customLineHeightData) { + this.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight); + } + this.commit(); + } + } + + set defaultLineHeight(defaultLineHeight: number) { + this._defaultLineHeight = defaultLineHeight; + } + + get defaultLineHeight() { + return this._defaultLineHeight; + } + + public removeCustomLineHeight(decorationID: string): void { + const customLines = this._decorationIDToCustomLine.get(decorationID); + if (!customLines) { + return; + } + this._decorationIDToCustomLine.delete(decorationID); + for (const customLine of customLines) { + customLine.deleted = true; + this._invalidIndex = Math.min(this._invalidIndex, customLine.index); + } + this._hasPending = true; + } + + public insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void { + this.removeCustomLineHeight(decorationId); + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const customLine = new CustomLine(decorationId, -1, lineNumber, lineHeight, 0); + this._pendingSpecialLinesToInsert.push(customLine); + } + this._hasPending = true; + } + + public heightForLineNumber(lineNumber: number): number { + const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber); + if (searchIndex >= 0) { + return this._orderedCustomLines[searchIndex].maximumSpecialHeight; + } + return this._defaultLineHeight; + } + + public getAccumulatedLineHeightsIncludingLineNumber(lineNumber: number): number { + const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber); + if (searchIndex >= 0) { + return this._orderedCustomLines[searchIndex].prefixSum + this._orderedCustomLines[searchIndex].maximumSpecialHeight; + } + if (searchIndex === -1) { + return this._defaultLineHeight * lineNumber; + } + const modifiedIndex = -(searchIndex + 1); + const previousSpecialLine = this._orderedCustomLines[modifiedIndex - 1]; + return previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (lineNumber - previousSpecialLine.lineNumber); + } + + public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { + const deleteCount = toLineNumber - fromLineNumber + 1; + const numberOfCustomLines = this._orderedCustomLines.length; + const candidateStartIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); + let startIndexOfDeletion: number; + if (candidateStartIndexOfDeletion >= 0) { + startIndexOfDeletion = candidateStartIndexOfDeletion; + for (let i = candidateStartIndexOfDeletion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + startIndexOfDeletion--; + } else { + break; + } + } + } else { + startIndexOfDeletion = candidateStartIndexOfDeletion === -(numberOfCustomLines + 1) && candidateStartIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateStartIndexOfDeletion + 1); + } + const candidateEndIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(toLineNumber); + let endIndexOfDeletion: number; + if (candidateEndIndexOfDeletion >= 0) { + endIndexOfDeletion = candidateEndIndexOfDeletion; + for (let i = candidateEndIndexOfDeletion + 1; i < numberOfCustomLines; i++) { + if (this._orderedCustomLines[i].lineNumber === toLineNumber) { + endIndexOfDeletion++; + } else { + break; + } + } + } else { + endIndexOfDeletion = candidateEndIndexOfDeletion === -(numberOfCustomLines + 1) && candidateEndIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateEndIndexOfDeletion + 1); + } + const isEndIndexBiggerThanStartIndex = endIndexOfDeletion > startIndexOfDeletion; + const isEndIndexEqualToStartIndexAndCoversCustomLine = endIndexOfDeletion === startIndexOfDeletion + && this._orderedCustomLines[startIndexOfDeletion] + && this._orderedCustomLines[startIndexOfDeletion].lineNumber >= fromLineNumber + && this._orderedCustomLines[startIndexOfDeletion].lineNumber <= toLineNumber; + + if (isEndIndexBiggerThanStartIndex || isEndIndexEqualToStartIndexAndCoversCustomLine) { + let maximumSpecialHeightOnDeletedInterval = 0; + for (let i = startIndexOfDeletion; i <= endIndexOfDeletion; i++) { + maximumSpecialHeightOnDeletedInterval = Math.max(maximumSpecialHeightOnDeletedInterval, this._orderedCustomLines[i].maximumSpecialHeight); + } + let prefixSumOnDeletedInterval = 0; + if (startIndexOfDeletion > 0) { + const previousSpecialLine = this._orderedCustomLines[startIndexOfDeletion - 1]; + prefixSumOnDeletedInterval = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (fromLineNumber - previousSpecialLine.lineNumber - 1); + } else { + prefixSumOnDeletedInterval = fromLineNumber > 0 ? (fromLineNumber - 1) * this._defaultLineHeight : 0; + } + const firstSpecialLineDeleted = this._orderedCustomLines[startIndexOfDeletion]; + const lastSpecialLineDeleted = this._orderedCustomLines[endIndexOfDeletion]; + const firstSpecialLineAfterDeletion = this._orderedCustomLines[endIndexOfDeletion + 1]; + const heightOfFirstLineAfterDeletion = firstSpecialLineAfterDeletion && firstSpecialLineAfterDeletion.lineNumber === toLineNumber + 1 ? firstSpecialLineAfterDeletion.maximumSpecialHeight : this._defaultLineHeight; + const totalHeightDeleted = lastSpecialLineDeleted.prefixSum + + lastSpecialLineDeleted.maximumSpecialHeight + - firstSpecialLineDeleted.prefixSum + + this._defaultLineHeight * (toLineNumber - lastSpecialLineDeleted.lineNumber) + + this._defaultLineHeight * (firstSpecialLineDeleted.lineNumber - fromLineNumber) + + heightOfFirstLineAfterDeletion - maximumSpecialHeightOnDeletedInterval; + + const decorationIdsSeen = new Set(); + const newOrderedCustomLines: CustomLine[] = []; + const newDecorationIDToSpecialLine = new ArrayMap(); + let numberOfDeletions = 0; + for (let i = 0; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + if (i < startIndexOfDeletion) { + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } else if (i >= startIndexOfDeletion && i <= endIndexOfDeletion) { + const decorationId = customLine.decorationId; + if (!decorationIdsSeen.has(decorationId)) { + customLine.index -= numberOfDeletions; + customLine.lineNumber = fromLineNumber; + customLine.prefixSum = prefixSumOnDeletedInterval; + customLine.maximumSpecialHeight = maximumSpecialHeightOnDeletedInterval; + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } else { + numberOfDeletions++; + } + } else if (i > endIndexOfDeletion) { + customLine.index -= numberOfDeletions; + customLine.lineNumber -= deleteCount; + customLine.prefixSum -= totalHeightDeleted; + newOrderedCustomLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + decorationIdsSeen.add(customLine.decorationId); + } + this._orderedCustomLines = newOrderedCustomLines; + this._decorationIDToCustomLine = newDecorationIDToSpecialLine; + } else { + const totalHeightDeleted = deleteCount * this._defaultLineHeight; + for (let i = endIndexOfDeletion; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + customLine.lineNumber -= deleteCount; + customLine.prefixSum -= totalHeightDeleted; + } + } + } + + public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + const insertCount = toLineNumber - fromLineNumber + 1; + const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); + let startIndexOfInsertion: number; + if (candidateStartIndexOfInsertion >= 0) { + startIndexOfInsertion = candidateStartIndexOfInsertion; + for (let i = candidateStartIndexOfInsertion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + startIndexOfInsertion--; + } else { + break; + } + } + } else { + startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1); + } + const toReAdd: ICustomLineHeightData[] = []; + const decorationsImmediatelyAfter = new Set(); + for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { + decorationsImmediatelyAfter.add(this._orderedCustomLines[i].decorationId); + } + } + const decorationsImmediatelyBefore = new Set(); + for (let i = startIndexOfInsertion - 1; i >= 0; i--) { + if (this._orderedCustomLines[i].lineNumber === fromLineNumber - 1) { + decorationsImmediatelyBefore.add(this._orderedCustomLines[i].decorationId); + } + } + const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter); + for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { + this._orderedCustomLines[i].lineNumber += insertCount; + this._orderedCustomLines[i].prefixSum += this._defaultLineHeight * insertCount; + } + + if (decorationsWithGaps.size > 0) { + for (const decorationId of decorationsWithGaps) { + const decoration = this._decorationIDToCustomLine.get(decorationId); + if (decoration) { + const startLineNumber = decoration.reduce((min, l) => Math.min(min, l.lineNumber), fromLineNumber); // min + const endLineNumber = decoration.reduce((max, l) => Math.max(max, l.lineNumber), fromLineNumber); // max + const lineHeight = decoration.reduce((max, l) => Math.max(max, l.specialHeight), 0); + toReAdd.push({ + decorationId, + startLineNumber, + endLineNumber, + lineHeight + }); + } + } + + for (const dec of toReAdd) { + this.insertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight); + } + this.commit(); + } + } + + public commit(): void { + if (!this._hasPending) { + return; + } + for (const pendingChange of this._pendingSpecialLinesToInsert) { + const candidateInsertionIndex = this._binarySearchOverOrderedCustomLinesArray(pendingChange.lineNumber); + const insertionIndex = candidateInsertionIndex >= 0 ? candidateInsertionIndex : -(candidateInsertionIndex + 1); + this._orderedCustomLines.splice(insertionIndex, 0, pendingChange); + this._invalidIndex = Math.min(this._invalidIndex, insertionIndex); + } + this._pendingSpecialLinesToInsert = []; + const newDecorationIDToSpecialLine = new ArrayMap(); + const newOrderedSpecialLines: CustomLine[] = []; + + for (let i = 0; i < this._invalidIndex; i++) { + const customLine = this._orderedCustomLines[i]; + newOrderedSpecialLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + + let numberOfDeletions = 0; + let previousSpecialLine: CustomLine | undefined = (this._invalidIndex > 0) ? newOrderedSpecialLines[this._invalidIndex - 1] : undefined; + for (let i = this._invalidIndex; i < this._orderedCustomLines.length; i++) { + const customLine = this._orderedCustomLines[i]; + if (customLine.deleted) { + numberOfDeletions++; + continue; + } + customLine.index = i - numberOfDeletions; + if (previousSpecialLine && previousSpecialLine.lineNumber === customLine.lineNumber) { + customLine.maximumSpecialHeight = previousSpecialLine.maximumSpecialHeight; + customLine.prefixSum = previousSpecialLine.prefixSum; + } else { + let maximumSpecialHeight = customLine.specialHeight; + for (let j = i; j < this._orderedCustomLines.length; j++) { + const nextSpecialLine = this._orderedCustomLines[j]; + if (nextSpecialLine.deleted) { + continue; + } + if (nextSpecialLine.lineNumber !== customLine.lineNumber) { + break; + } + maximumSpecialHeight = Math.max(maximumSpecialHeight, nextSpecialLine.specialHeight); + } + customLine.maximumSpecialHeight = maximumSpecialHeight; + + let prefixSum: number; + if (previousSpecialLine) { + prefixSum = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (customLine.lineNumber - previousSpecialLine.lineNumber - 1); + } else { + prefixSum = this._defaultLineHeight * (customLine.lineNumber - 1); + } + customLine.prefixSum = prefixSum; + } + previousSpecialLine = customLine; + newOrderedSpecialLines.push(customLine); + newDecorationIDToSpecialLine.add(customLine.decorationId, customLine); + } + this._orderedCustomLines = newOrderedSpecialLines; + this._decorationIDToCustomLine = newDecorationIDToSpecialLine; + this._invalidIndex = Infinity; + this._hasPending = false; + } + + private _binarySearchOverOrderedCustomLinesArray(lineNumber: number): number { + return binarySearch2(this._orderedCustomLines.length, (index) => { + const line = this._orderedCustomLines[index]; + if (line.lineNumber === lineNumber) { + return 0; + } else if (line.lineNumber < lineNumber) { + return -1; + } else { + return 1; + } + }); + } +} + +export interface ICustomLineHeightData { + readonly decorationId: string; + readonly startLineNumber: number; + readonly endLineNumber: number; + readonly lineHeight: number; +} + +class ArrayMap { + + private _map: Map = new Map(); + + constructor() { } + + add(key: K, value: T) { + const array = this._map.get(key); + if (!array) { + this._map.set(key, [value]); + } else { + array.push(value); + } + } + + get(key: K): T[] | undefined { + return this._map.get(key); + } + + delete(key: K): void { + this._map.delete(key); + } +} diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index f7988abddce..b773e5dd4cb 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorWhitespace, IPartialViewLinesViewportData, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; +import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; import * as strings from '../../../base/common/strings.js'; +import { ICustomLineHeightData, LineHeightsManager } from './lineHeights.js'; interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number } interface IPendingRemove { id: string } @@ -37,10 +38,6 @@ class PendingChanges { this._removes.push(x); } - public mustCommit(): boolean { - return this._hasPending; - } - public commit(linesLayout: LinesLayout): void { if (!this._hasPending) { return; @@ -94,11 +91,11 @@ export class LinesLayout { private _prefixSumValidIndex: number; private _minWidth: number; private _lineCount: number; - private _lineHeight: number; private _paddingTop: number; private _paddingBottom: number; + private _lineHeightsManager: LineHeightsManager; - constructor(lineCount: number, lineHeight: number, paddingTop: number, paddingBottom: number) { + constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: ICustomLineHeightData[]) { this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT); this._pendingChanges = new PendingChanges(); this._lastWhitespaceId = 0; @@ -106,9 +103,9 @@ export class LinesLayout { this._prefixSumValidIndex = -1; this._minWidth = -1; /* marker for not being computed */ this._lineCount = lineCount; - this._lineHeight = lineHeight; this._paddingTop = paddingTop; this._paddingBottom = paddingBottom; + this._lineHeightsManager = new LineHeightsManager(defaultLineHeight, customLineHeightData); } /** @@ -141,9 +138,8 @@ export class LinesLayout { /** * Change the height of a line in pixels. */ - public setLineHeight(lineHeight: number): void { - this._checkPendingChanges(); - this._lineHeight = lineHeight; + public setDefaultLineHeight(lineHeight: number): void { + this._lineHeightsManager.defaultLineHeight = lineHeight; } /** @@ -159,9 +155,29 @@ export class LinesLayout { * * @param lineCount New number of lines. */ - public onFlushed(lineCount: number): void { - this._checkPendingChanges(); + public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { this._lineCount = lineCount; + this._lineHeightsManager = new LineHeightsManager(this._lineHeightsManager.defaultLineHeight, customLineHeightData); + } + + public changeLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean { + let hadAChange = false; + try { + const accessor: ILineHeightChangeAccessor = { + insertOrChangeCustomLineHeight: (decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void => { + hadAChange = true; + this._lineHeightsManager.insertOrChangeCustomLineHeight(decorationId, startLineNumber, endLineNumber, lineHeight); + }, + removeCustomLineHeight: (decorationId: string): void => { + hadAChange = true; + this._lineHeightsManager.removeCustomLineHeight(decorationId); + } + }; + callback(accessor); + } finally { + this._lineHeightsManager.commit(); + } + return hadAChange; } public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean { @@ -259,12 +275,6 @@ export class LinesLayout { this._prefixSumValidIndex = -1; } - private _checkPendingChanges(): void { - if (this._pendingChanges.mustCommit()) { - this._pendingChanges.commit(this); - } - } - private _insertWhitespace(whitespace: EditorWhitespace): void { const insertIndex = LinesLayout.findInsertionIndex(this._arr, whitespace.afterLineNumber, whitespace.ordinal); this._arr.splice(insertIndex, 0, whitespace); @@ -318,7 +328,6 @@ export class LinesLayout { * @param toLineNumber The line number at which the deletion ended, inclusive */ public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { - this._checkPendingChanges(); fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -336,6 +345,7 @@ export class LinesLayout { this._arr[i].afterLineNumber -= (toLineNumber - fromLineNumber + 1); } } + this._lineHeightsManager.onLinesDeleted(fromLineNumber, toLineNumber); } /** @@ -345,7 +355,6 @@ export class LinesLayout { * @param toLineNumber The line number at which the insertion ended, inclusive. */ public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { - this._checkPendingChanges(); fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -357,13 +366,13 @@ export class LinesLayout { this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); } } + this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber); } /** * Get the sum of all the whitespaces. */ public getWhitespacesTotalHeight(): number { - this._checkPendingChanges(); if (this._arr.length === 0) { return 0; } @@ -378,7 +387,6 @@ export class LinesLayout { * @return The sum of the heights of all whitespaces before the one at `index`, including the one at `index`. */ public getWhitespacesAccumulatedHeight(index: number): number { - this._checkPendingChanges(); index = index | 0; let startIndex = Math.max(0, this._prefixSumValidIndex + 1); @@ -400,8 +408,7 @@ export class LinesLayout { * @return The sum of heights for all objects. */ public getLinesTotalHeight(): number { - this._checkPendingChanges(); - const linesHeight = this._lineHeight * this._lineCount; + const linesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(this._lineCount); const whitespacesHeight = this.getWhitespacesTotalHeight(); return linesHeight + whitespacesHeight + this._paddingTop + this._paddingBottom; @@ -413,7 +420,6 @@ export class LinesLayout { * @param lineNumber The line number */ public getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber: number): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); @@ -470,7 +476,6 @@ export class LinesLayout { * @return The index of the first whitespace with `afterLineNumber` >= `lineNumber` or -1 if no whitespace is found. */ public getFirstWhitespaceIndexAfterLineNumber(lineNumber: number): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; return this._findFirstWhitespaceAfterLineNumber(lineNumber); @@ -483,12 +488,11 @@ export class LinesLayout { * @return The sum of heights for all objects above `lineNumber`. */ public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones = false): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; let previousLinesHeight: number; if (lineNumber > 1) { - previousLinesHeight = this._lineHeight * (lineNumber - 1); + previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber - 1); } else { previousLinesHeight = 0; } @@ -498,16 +502,19 @@ export class LinesLayout { return previousLinesHeight + previousWhitespacesHeight + this._paddingTop; } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._lineHeightsManager.heightForLineNumber(lineNumber); + } + /** - * Get the vertical offset (the sum of heights for all objects above) a certain line number. + * Get the vertical offset (the sum of heights for all objects above) a certain line number and also the line height of the line. * * @param lineNumber The line number * @return The sum of heights for all objects above `lineNumber`. */ public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones = false): number { - this._checkPendingChanges(); lineNumber = lineNumber | 0; - const previousLinesHeight = this._lineHeight * lineNumber; + const previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber); const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber + (includeViewZones ? 1 : 0)); return previousLinesHeight + previousWhitespacesHeight + this._paddingTop; } @@ -516,7 +523,6 @@ export class LinesLayout { * Returns if there is any whitespace in the document. */ public hasWhitespace(): boolean { - this._checkPendingChanges(); return this.getWhitespacesCount() > 0; } @@ -524,7 +530,6 @@ export class LinesLayout { * The maximum min width for all whitespaces. */ public getWhitespaceMinWidth(): number { - this._checkPendingChanges(); if (this._minWidth === -1) { let minWidth = 0; for (let i = 0, len = this._arr.length; i < len; i++) { @@ -539,7 +544,6 @@ export class LinesLayout { * Check if `verticalOffset` is below all lines. */ public isAfterLines(verticalOffset: number): boolean { - this._checkPendingChanges(); const totalHeight = this.getLinesTotalHeight(); return verticalOffset > totalHeight; } @@ -548,7 +552,6 @@ export class LinesLayout { if (this._paddingTop === 0) { return false; } - this._checkPendingChanges(); return (verticalOffset < this._paddingTop); } @@ -556,7 +559,6 @@ export class LinesLayout { if (this._paddingBottom === 0) { return false; } - this._checkPendingChanges(); const totalHeight = this.getLinesTotalHeight(); return (verticalOffset >= totalHeight - this._paddingBottom); } @@ -570,7 +572,6 @@ export class LinesLayout { * @return The line number at or after vertical offset `verticalOffset`. */ public getLineNumberAtOrAfterVerticalOffset(verticalOffset: number): number { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; if (verticalOffset < 0) { @@ -578,13 +579,13 @@ export class LinesLayout { } const linesCount = this._lineCount | 0; - const lineHeight = this._lineHeight; let minLineNumber = 1; let maxLineNumber = linesCount; while (minLineNumber < maxLineNumber) { const midLineNumber = ((minLineNumber + maxLineNumber) / 2) | 0; + const lineHeight = this.getLineHeightForLineNumber(midLineNumber); const midLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(midLineNumber) | 0; if (verticalOffset >= midLineNumberVerticalOffset + lineHeight) { @@ -614,10 +615,8 @@ export class LinesLayout { * @return A structure describing the lines positioned between `verticalOffset1` and `verticalOffset2`. */ public getLinesViewportData(verticalOffset1: number, verticalOffset2: number): IPartialViewLinesViewportData { - this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; - const lineHeight = this._lineHeight; // Find first line number // We don't live in a perfect world, so the line number might start before or after verticalOffset1 @@ -650,7 +649,7 @@ export class LinesLayout { if (startLineNumberVerticalOffset >= STEP_SIZE) { // Compute a delta that guarantees that lines are positioned at `lineHeight` increments bigNumbersDelta = Math.floor(startLineNumberVerticalOffset / STEP_SIZE) * STEP_SIZE; - bigNumbersDelta = Math.floor(bigNumbersDelta / lineHeight) * lineHeight; + bigNumbersDelta = Math.floor(bigNumbersDelta / this._lineHeightsManager.defaultLineHeight) * this._lineHeightsManager.defaultLineHeight; currentLineRelativeOffset -= bigNumbersDelta; } @@ -662,7 +661,7 @@ export class LinesLayout { // Figure out how far the lines go for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - + const lineHeight = this.getLineHeightForLineNumber(lineNumber); if (centeredLineNumber === -1) { const currentLineTop = currentVerticalOffset; const currentLineBottom = currentVerticalOffset + lineHeight; @@ -715,7 +714,8 @@ export class LinesLayout { } } if (completelyVisibleStartLineNumber < completelyVisibleEndLineNumber) { - if (endLineNumberVerticalOffset + lineHeight > verticalOffset2) { + const endLineHeight = this.getLineHeightForLineNumber(endLineNumber); + if (endLineNumberVerticalOffset + endLineHeight > verticalOffset2) { completelyVisibleEndLineNumber--; } } @@ -728,19 +728,18 @@ export class LinesLayout { centeredLineNumber: centeredLineNumber, completelyVisibleStartLineNumber: completelyVisibleStartLineNumber, completelyVisibleEndLineNumber: completelyVisibleEndLineNumber, - lineHeight: this._lineHeight, + lineHeight: this._lineHeightsManager.defaultLineHeight, }; } public getVerticalOffsetForWhitespaceIndex(whitespaceIndex: number): number { - this._checkPendingChanges(); whitespaceIndex = whitespaceIndex | 0; const afterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex); let previousLinesHeight: number; if (afterLineNumber >= 1) { - previousLinesHeight = this._lineHeight * afterLineNumber; + previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(afterLineNumber); } else { previousLinesHeight = 0; } @@ -755,7 +754,6 @@ export class LinesLayout { } public getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset: number): number { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; let minWhitespaceIndex = 0; @@ -799,7 +797,6 @@ export class LinesLayout { * @return Precisely the whitespace that is layouted at `verticaloffset` or null. */ public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null { - this._checkPendingChanges(); verticalOffset = verticalOffset | 0; const candidateIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset); @@ -838,7 +835,6 @@ export class LinesLayout { * @return An array with all the whitespaces in the viewport. If no whitespace is in viewport, the array is empty. */ public getWhitespaceViewportData(verticalOffset1: number, verticalOffset2: number): IViewWhitespaceViewportData[] { - this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; @@ -872,7 +868,6 @@ export class LinesLayout { * Get all whitespaces. */ public getWhitespaces(): IEditorWhitespace[] { - this._checkPendingChanges(); return this._arr.slice(0); } @@ -880,7 +875,6 @@ export class LinesLayout { * The number of whitespaces. */ public getWhitespacesCount(): number { - this._checkPendingChanges(); return this._arr.length; } @@ -891,7 +885,6 @@ export class LinesLayout { * @return `id` of whitespace at `index`. */ public getIdForWhitespaceIndex(index: number): string { - this._checkPendingChanges(); index = index | 0; return this._arr[index].id; @@ -904,7 +897,6 @@ export class LinesLayout { * @return `afterLineNumber` of whitespace at `index`. */ public getAfterLineNumberForWhitespaceIndex(index: number): number { - this._checkPendingChanges(); index = index | 0; return this._arr[index].afterLineNumber; @@ -917,7 +909,6 @@ export class LinesLayout { * @return `height` of whitespace at `index`. */ public getHeightForWhitespaceIndex(index: number): number { - this._checkPendingChanges(); index = index | 0; return this._arr[index].height; diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 767a308db9a..404a5823b04 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -10,8 +10,9 @@ import { ConfigurationChangedEvent, EditorOption } from '../config/editorOptions import { ScrollType } from '../editorCommon.js'; import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { LinesLayout } from './linesLayout.js'; -import { IEditorWhitespace, IPartialViewLinesViewportData, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; +import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js'; +import { ICustomLineHeightData } from './lineHeights.js'; const SMOOTH_SCROLLING_TIME = 125; @@ -163,7 +164,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public readonly onDidScroll: Event; public readonly onDidContentSizeChange: Event; - constructor(configuration: IEditorConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this._configuration = configuration; @@ -171,7 +172,7 @@ export class ViewLayout extends Disposable implements IViewLayout { const layoutInfo = options.get(EditorOption.layoutInfo); const padding = options.get(EditorOption.padding); - this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom); + this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom, customLineHeightData); this._maxLineWidth = 0; this._overlayWidgetsMinWidth = 0; @@ -211,7 +212,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public onConfigurationChanged(e: ConfigurationChangedEvent): void { const options = this._configuration.options; if (e.hasChanged(EditorOption.lineHeight)) { - this._linesLayout.setLineHeight(options.get(EditorOption.lineHeight)); + this._linesLayout.setDefaultLineHeight(options.get(EditorOption.lineHeight)); } if (e.hasChanged(EditorOption.padding)) { const padding = options.get(EditorOption.padding); @@ -236,8 +237,8 @@ export class ViewLayout extends Disposable implements IViewLayout { this._configureSmoothScrollDuration(); } } - public onFlushed(lineCount: number): void { - this._linesLayout.onFlushed(lineCount); + public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + this._linesLayout.onFlushed(lineCount, customLineHeightData); } public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber); @@ -380,12 +381,24 @@ export class ViewLayout extends Disposable implements IViewLayout { } return hadAChange; } + + public changeSpecialLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean { + const hadAChange = this._linesLayout.changeLineHeights(callback); + if (hadAChange) { + this.onHeightMaybeChanged(); + } + return hadAChange; + } + public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones: boolean = false): number { return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones); } public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones: boolean = false): number { return this._linesLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones); } + public getLineHeightForLineNumber(lineNumber: number): number { + return this._linesLayout.getLineHeightForLineNumber(lineNumber); + } public isAfterLines(verticalOffset: number): boolean { return this._linesLayout.isAfterLines(verticalOffset); } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 1aefa04abeb..d9233c5c06c 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -133,6 +133,7 @@ export interface IViewLayout { getLineNumberAtVerticalOffset(verticalOffset: number): number; getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones?: boolean): number; getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones?: boolean): number; + getLineHeightForLineNumber(lineNumber: number): number; getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null; /** @@ -156,6 +157,11 @@ export interface IWhitespaceChangeAccessor { removeWhitespace(id: string): void; } +export interface ILineHeightChangeAccessor { + insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void; + removeCustomLineHeight(decorationId: string): void; +} + export interface IPartialViewLinesViewportData { /** * Value to be substracted from `scrollTop` (in order to vertical offset numbers < 1MM) diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 7fe34c92995..09d842ecd35 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -34,12 +34,13 @@ import { ViewLayout } from '../viewLayout/viewLayout.js'; import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js'; import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; import { ViewEventHandler } from '../viewEventHandler.js'; -import { ICoordinatesConverter, InlineDecoration, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; +import { ICoordinatesConverter, InlineDecoration, ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; -import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; +import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelLineHeightChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; +import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -116,7 +117,7 @@ export class ViewModel extends Disposable implements IViewModel { this._cursor = this._register(new CursorsController(model, this, this.coordinatesConverter, this.cursorConfig)); - this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); + this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), this._getCustomLineHeights(), scheduleAtNextAnimationFrame)); this._register(this.viewLayout.onDidScroll((e) => { if (e.scrollTopChanged) { @@ -183,6 +184,20 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.removeViewEventHandler(eventHandler); } + private _getCustomLineHeights(): ICustomLineHeightData[] { + const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); + return decorations.map((d) => { + const lineNumber = d.range.startLineNumber; + const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); + return { + decorationId: d.id, + startLineNumber: viewRange.startLineNumber, + endLineNumber: viewRange.endLineNumber, + lineHeight: d.options.lineHeight || 0 + }; + }); + } + private _updateConfigurationViewLineCountNow(): void { this._configuration.setViewLineCount(this._lines.getViewLineCount()); } @@ -254,7 +269,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); this._updateConfigurationViewLineCount.schedule(); } @@ -327,7 +342,7 @@ export class ViewModel extends Disposable implements IViewModel { this._lines.onModelFlushed(); eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); this._decorations.reset(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); hadOtherModelChange = true; break; } @@ -419,6 +434,28 @@ export class ViewModel extends Disposable implements IViewModel { this._handleVisibleLinesChanged(); })); + this._register(this.model.onDidChangeLineHeight((e) => { + const filteredChanges = e.changes.filter((change) => change.ownerId === this._editorId || change.ownerId === 0); + + this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { + for (const change of filteredChanges) { + const { decorationId, lineNumber, lineHeight } = change; + const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); + if (lineHeight !== null) { + accessor.insertOrChangeCustomLineHeight(decorationId, viewRange.startLineNumber, viewRange.endLineNumber, lineHeight); + } else { + accessor.removeCustomLineHeight(decorationId); + } + } + }); + + // recreate the model event using the filtered changes + if (filteredChanges.length > 0) { + const filteredEvent = new textModelEvents.ModelLineHeightChangedEvent(filteredChanges); + this._eventDispatcher.emitOutgoingEvent(new ModelLineHeightChangedEvent(filteredEvent)); + } + })); + this._register(this.model.onDidChangeTokens((e) => { const viewRanges: { fromLineNumber: number; toLineNumber: number }[] = []; for (let j = 0, lenJ = e.ranges.length; j < lenJ; j++) { @@ -457,7 +494,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); } finally { this._eventDispatcher.endEmitViewEvents(); } @@ -506,7 +543,7 @@ export class ViewModel extends Disposable implements IViewModel { eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); this._cursor.onLineMappingChanged(eventsCollector); this._decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + this.viewLayout.onFlushed(this.getLineCount(), this._getCustomLineHeights()); this.viewLayout.onHeightMaybeChanged(); } diff --git a/src/vs/editor/common/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModelEventDispatcher.ts index fc91875b319..81516adf725 100644 --- a/src/vs/editor/common/viewModelEventDispatcher.ts +++ b/src/vs/editor/common/viewModelEventDispatcher.ts @@ -10,7 +10,7 @@ import { Emitter } from '../../base/common/event.js'; import { Selection } from './core/selection.js'; import { Disposable } from '../../base/common/lifecycle.js'; import { CursorChangeReason } from './cursorEvents.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from './textModelEvents.js'; +import { ModelLineHeightChangedEvent as OriginalModelLineHeightChangedEvent, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from './textModelEvents.js'; export class ViewModelEventDispatcher extends Disposable { @@ -188,6 +188,7 @@ export type OutgoingViewModelEvent = ( | ModelContentChangedEvent | ModelOptionsChangedEvent | ModelTokensChangedEvent + | ModelLineHeightChangedEvent ); export const enum OutgoingViewModelEventKind { @@ -205,6 +206,7 @@ export const enum OutgoingViewModelEventKind { ModelContentChanged, ModelOptionsChanged, ModelTokensChanged, + ModelLineHeightChanged, } export class ContentSizeChangedEvent implements IContentSizeChangedEvent { @@ -553,3 +555,19 @@ export class ModelTokensChangedEvent { return null; } } + +export class ModelLineHeightChangedEvent { + public readonly kind = OutgoingViewModelEventKind.ModelLineHeightChanged; + + constructor( + public readonly event: OriginalModelLineHeightChangedEvent + ) { } + + public isNoOp(): boolean { + return false; + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + return null; + } +} diff --git a/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts b/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts index e02392bdad9..c1cd8f14db4 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts @@ -19,7 +19,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, IStatusHandle } from '../../../../platform/notification/common/notification.js'; export const ctxHasSymbols = new RawContextKey('hasSymbols', false, localize('hasSymbols', "Whether there are symbol locations that can be navigated via keyboard-only.")); @@ -41,7 +41,7 @@ class SymbolNavigationService implements ISymbolNavigationService { private _currentModel?: ReferencesModel = undefined; private _currentIdx: number = -1; private _currentState?: IDisposable; - private _currentMessage?: IDisposable; + private _currentMessage?: IStatusHandle; private _ignoreEditorChange: boolean = false; constructor( @@ -56,7 +56,7 @@ class SymbolNavigationService implements ISymbolNavigationService { reset(): void { this._ctxHasSymbols.reset(); this._currentState?.dispose(); - this._currentMessage?.dispose(); + this._currentMessage?.close(); this._currentModel = undefined; this._currentIdx = -1; } @@ -138,7 +138,7 @@ class SymbolNavigationService implements ISymbolNavigationService { private _showMessage(): void { - this._currentMessage?.dispose(); + this._currentMessage?.close(); const kb = this._keybindingService.lookupKeybinding('editor.gotoNextSymbolFromResult'); const message = kb diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index e467efa9ac0..41e9b7fbd32 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -386,7 +386,7 @@ export class AutoIndentOnPaste implements IEditorContribution { this.callOnModel.clear(); // we are disabled - if (this.editor.getOption(EditorOption.autoIndent) < EditorAutoIndentStrategy.Full || this.editor.getOption(EditorOption.formatOnPaste)) { + if (this.editor.getOption(EditorOption.autoIndent) < EditorAutoIndentStrategy.Full || !this.editor.getOption(EditorOption.formatOnPaste)) { return; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 18c567f7c2c..e124b19e2b3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -262,8 +262,8 @@ export class InlineCompletionsController extends Disposable { await timeout(50, cancelOnDispose(store)); await waitForState(this._suggestWidgetAdapter.selectedItem, isUndefined, () => false, cancelOnDispose(store)); + await this._accessibilitySignalService.playSignal(state.kind === 'ghostText' ? AccessibilitySignal.inlineSuggestion : AccessibilitySignal.nextEditSuggestion); - await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { if (state.kind === 'ghostText') { this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index a5927435aa8..b92e6b7ba44 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -315,7 +315,7 @@ export class InlineCompletionsModel extends Disposable { const providers = changeSummary.provider ? [changeSummary.provider] : this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel); - return this._source.fetch(providers, cursorPosition, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion); + return this._source.fetch(providers, cursorPosition, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, !!changeSummary.provider); }); public async trigger(tx?: ITransaction, options?: { onlyFetchInlineEdits?: boolean; noDelay?: boolean }): Promise { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index ed3a57bc5ba..9ff5450279c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -107,12 +107,12 @@ export class InlineCompletionsSource extends Disposable { private readonly _loadingCount = observableValue(this, 0); public readonly loading = this._loadingCount.map(this, v => v > 0); - public fetch(providers: InlineCompletionsProvider[], position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable): Promise { + public fetch(providers: InlineCompletionsProvider[], position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable, providerhasChangedCompletion: boolean): Promise { const request = new UpdateRequest(position, context, this._textModel.getVersionId()); const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get(); - if (this._updateOperation.value?.request.satisfies(request)) { + if (!providerhasChangedCompletion && this._updateOperation.value?.request.satisfies(request)) { return this._updateOperation.value.promise; } else if (target?.request?.satisfies(request)) { return Promise.resolve(true); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index e81ea290e79..adc37495bd8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -12,7 +12,7 @@ import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; -import { getPositionOffsetTransformerFromTextModel } from '../../../../common/core/positionToOffset.js'; +import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js'; import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit, StringText, TextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; @@ -20,7 +20,7 @@ import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.j import { InlineCompletion, InlineCompletionTriggerKind, Command, InlineCompletionWarning, PartialAcceptInfo, InlineCompletionEndOfLifeReason } from '../../../../common/languages.js'; import { ITextModel, EndOfLinePreference } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; -import { IDisplayLocation as IInlineCompletionDisplayLocation, InlineSuggestData, InlineSuggestionList, SnippetInfo } from './provideInlineCompletions.js'; +import { IDisplayLocation, InlineSuggestData, InlineSuggestionList, SnippetInfo } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem; @@ -42,6 +42,7 @@ abstract class InlineSuggestionItemBase { constructor( protected readonly _data: InlineSuggestData, public readonly identity: InlineSuggestionIdentity, + public readonly displayLocation: InlineSuggestDisplayLocation | undefined ) { } /** @@ -60,7 +61,6 @@ abstract class InlineSuggestionItemBase { public get command(): Command | undefined { return this._sourceInlineCompletion.command; } public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } - public get displayLocation(): IInlineCompletionDisplayLocation | undefined { return this._data.displayLocation; } public get hash() { return JSON.stringify([ this.getSingleTextEdit().text, @@ -140,6 +140,43 @@ export class InlineSuggestionIdentity { } } +class InlineSuggestDisplayLocation implements IDisplayLocation { + + public static create(displayLocation: IDisplayLocation, textmodel: ITextModel) { + const offsetRange = new OffsetRange( + textmodel.getOffsetAt(displayLocation.range.getStartPosition()), + textmodel.getOffsetAt(displayLocation.range.getEndPosition()) + ); + + return new InlineSuggestDisplayLocation( + offsetRange, + displayLocation.range, + displayLocation.label, + ); + } + + private constructor( + private readonly _offsetRange: OffsetRange, + public readonly range: Range, + public readonly label: string, + ) { } + + public withEdit(edit: OffsetEdit, positionOffsetTransformer: PositionOffsetTransformer): InlineSuggestDisplayLocation | undefined { + const newOffsetRange = applyEditsToRanges([this._offsetRange], edit)[0]; + if (!newOffsetRange || newOffsetRange.length !== this._offsetRange.length) { + return undefined; + } + + const newRange = positionOffsetTransformer.getRange(newOffsetRange); + + return new InlineSuggestDisplayLocation( + newOffsetRange, + newRange, + this.label, + ); + } +} + export class InlineCompletionItem extends InlineSuggestionItemBase { public static create( data: InlineSuggestData, @@ -148,8 +185,9 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { const identity = new InlineSuggestionIdentity(); const textEdit = new SingleTextEdit(data.range, data.insertText); const edit = getPositionOffsetTransformerFromTextModel(textModel).getSingleOffsetEdit(textEdit); + const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; - return new InlineCompletionItem(edit, textEdit, data.range, data.snippetInfo, data.additionalTextEdits, data, identity); + return new InlineCompletionItem(edit, textEdit, data.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); } public readonly isInlineEdit = false; @@ -163,8 +201,9 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { data: InlineSuggestData, identity: InlineSuggestionIdentity, + displayLocation: InlineSuggestDisplayLocation | undefined, ) { - super(data, identity); + super(data, identity, displayLocation); } override getSingleTextEdit(): SingleTextEdit { return this._textEdit; } @@ -178,6 +217,7 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this.additionalTextEdits, this._data, identity, + this.displayLocation ); } @@ -187,7 +227,17 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { return undefined; } const newEdit = new SingleOffsetEdit(newEditRange[0], this._textEdit.text); - const newTextEdit = getPositionOffsetTransformerFromTextModel(textModel).getSingleTextEdit(newEdit); + const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); + const newTextEdit = positionOffsetTransformer.getSingleTextEdit(newEdit); + + let newDisplayLocation = this.displayLocation; + if (newDisplayLocation) { + newDisplayLocation = newDisplayLocation.withEdit(textModelEdit, positionOffsetTransformer); + if (!newDisplayLocation) { + return undefined; + } + } + return new InlineCompletionItem( newEdit, newTextEdit, @@ -196,6 +246,7 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this.additionalTextEdits, this._data, this.identity, + newDisplayLocation ); } @@ -265,7 +316,8 @@ export class InlineEditItem extends InlineSuggestionItemBase { const replacedText = textModel.getValueInRange(replacedRange); return SingleUpdatedNextEdit.create(edit, replacedText); }); - return new InlineEditItem(offsetEdit, singleTextEdit, data, identity, edits, false, textModel.getVersionId()); + const displayLocation = data.displayLocation ? InlineSuggestDisplayLocation.create(data.displayLocation, textModel) : undefined; + return new InlineEditItem(offsetEdit, singleTextEdit, data, identity, edits, displayLocation, false, textModel.getVersionId()); } public readonly snippetInfo: SnippetInfo | undefined = undefined; @@ -280,10 +332,11 @@ export class InlineEditItem extends InlineSuggestionItemBase { identity: InlineSuggestionIdentity, private readonly _edits: readonly SingleUpdatedNextEdit[], + displayLocation: InlineSuggestDisplayLocation | undefined, private readonly _lastChangePartOfInlineEdit = false, private readonly _inlineEditModelVersion: number, ) { - super(data, identity); + super(data, identity, displayLocation); } public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; } @@ -300,6 +353,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { this._data, identity, this._edits, + this.displayLocation, this._lastChangePartOfInlineEdit, this._inlineEditModelVersion, ); @@ -339,16 +393,25 @@ export class InlineEditItem extends InlineSuggestionItemBase { return undefined; // the completion has been typed by the user } - const edit = new OffsetEdit(edits.map(edit => edit.edit!)); + const newEdit = new OffsetEdit(edits.map(edit => edit.edit!)); + const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); + const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toSingle(new TextModelText(textModel)); - const textEdit = getPositionOffsetTransformerFromTextModel(textModel).getTextEdit(edit).toSingle(new TextModelText(textModel)); + let newDisplayLocation = this.displayLocation; + if (newDisplayLocation) { + newDisplayLocation = newDisplayLocation.withEdit(textModelChanges, positionOffsetTransformer); + if (!newDisplayLocation) { + return undefined; + } + } return new InlineEditItem( - edit, - textEdit, + newEdit, + newTextEdit, this._data, this.identity, edits, + newDisplayLocation, lastChangePartOfInlineEdit, inlineEditModelVersion, ); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 4e0a4c88323..57d3134c250 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -6,7 +6,7 @@ import { ChildNode, LiveElement, n } from '../../../../../../../base/browser/dom.js'; import { ActionBar, IActionBarOptions } from '../../../../../../../base/browser/ui/actionbar/actionbar.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { KeybindingLabel } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IAction } from '../../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../../../../../base/common/keybindings.js'; @@ -18,7 +18,8 @@ import { ICommandService } from '../../../../../../../platform/commands/common/c import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { nativeHoverDelegate } from '../../../../../../../platform/hover/browser/hover.js'; import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; -import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { defaultKeybindingLabelStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; +import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder, keybindingLabelBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; @@ -192,9 +193,16 @@ function option(props: { }, [ThemeIcon.isThemeIcon(props.icon) ? renderIcon(props.icon) : props.icon.map(icon => renderIcon(icon))]), n.elem('span', {}, [props.title]), n.div({ - style: { marginLeft: 'auto', opacity: '0.6' }, + style: { marginLeft: 'auto' }, ref: elem => { - const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); + const keybindingLabel = store.add(new KeybindingLabel(elem, OS, { + disableTitle: true, + ...defaultKeybindingLabelStyles, + keybindingLabelShadow: undefined, + keybindingLabelBackground: asCssVariable(keybindingLabelBackground), + keybindingLabelBorder: 'transparent', + keybindingLabelBottomBorder: undefined, + })); store.add(autorun(reader => { keybindingLabel.set(props.keybinding.read(reader)); })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index 9c5e9293ee6..fc90fe16e58 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -438,7 +438,7 @@ export class InlineEditsGutterIndicator extends Disposable { }, style: { cursor: 'pointer', - zIndex: '1000', + zIndex: '20', position: 'absolute', backgroundColor: this._gutterIndicatorStyles.map(v => v.background), ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index 0c0e2eb6f1b..cdd3bd12518 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -364,7 +364,12 @@ export class InlineEditsView extends Disposable { return 'wordReplacements'; } } + if (numOriginalLines > 0 && numModifiedLines > 0) { + if (numOriginalLines === 1 && numModifiedLines === 1) { + return 'lineReplacement'; + } + if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, this._previewTextModel, inlineEdit, reader)) { return 'sideBySide'; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts index f81f5278a50..458aacf7533 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts @@ -57,7 +57,6 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: 'block', }, }, [ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts index 43e382a39f9..16fad4d39ac 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts @@ -73,30 +73,29 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie const view = state.map(s => s ? this.getRendering(s, styles) : undefined); - this._register(autorun((reader) => { - const v = view.read(reader); - if (!v) { this._isHovered.set(false, undefined); return; } - this._isHovered.set(v.isHovered.read(reader), undefined); - })); - - const overlayElement = n.div({ + const overlay = n.div({ class: 'inline-edits-custom-view', style: { position: 'absolute', overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: 'block', }, - }, [view]).keepUpdated(this._store).element; + }, [view]).keepUpdated(this._store); this._register(this._editorObs.createOverlayWidget({ - domNode: overlayElement, + domNode: overlay.element, position: constObservable(null), allowEditorOverflow: false, minContentWidthInPx: constObservable(0), })); + + this._register(autorun((reader) => { + const v = view.read(reader); + if (!v) { this._isHovered.set(false, undefined); return; } + this._isHovered.set(overlay.isHovered.read(reader), undefined); + })); } private getState(displayLocation: InlineCompletionDisplayLocation): { rect: IObservable; label: string } { @@ -234,7 +233,6 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie onclick: (e) => { this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); } }, [ line - ]).keepUpdated(this._store); - + ]); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts index fcd48453e74..253d45b1ea5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts @@ -198,7 +198,6 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index e79107bbd24..6cafdf6f116 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -302,7 +302,6 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index 68ad9e48ad8..9e30a4860dd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -581,7 +581,6 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit overflow: 'visible', top: '0px', left: '0px', - zIndex: '0', display: this._display, }, }, [ diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 0c4c59ff27e..9f443e037d6 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -96,6 +96,14 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._widgetState = StickyScrollWidgetState.Empty; const stickyScrollDomNode = this._stickyScrollWidget.getDomNode(); + this._register(this._editor.onDidChangeLineHeight((e) => { + e.changes.forEach((change) => { + const lineNumber = change.lineNumber; + if (this._widgetState.startLineNumbers.includes(lineNumber)) { + this._renderStickyScroll(lineNumber); + } + }); + })); this._register(this._editor.onDidChangeConfiguration(e => { this._readConfigurationChange(e); })); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 4fbe34be92f..24f9a4c279d 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -162,7 +162,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi } } const lowerBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber, (a: number, b: number) => { return a - b; })); - const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber + depth, (a: number, b: number) => { return a - b; })); + const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.endLineNumber, (a: number, b: number) => { return a - b; })); for (let i = lowerBound; i <= upperBound; i++) { const child = outlineModel.children[i]; @@ -175,7 +175,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi const childEndLine = childRange.endLineNumber; if (range.startLineNumber <= childEndLine + 1 && childStartLine - 1 <= range.endLineNumber && childStartLine !== lastLine) { lastLine = childStartLine; - const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const lineHeight = this._editor.getLineHeightForLineNumber(childStartLine); result.push(new StickyLineCandidate(childStartLine, childEndLine - 1, top, lineHeight)); this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, childStartLine); } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 9ec68fb92a8..ddda2843eb2 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -160,7 +160,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { if (!state) { return true; } - const futureWidgetHeight = state.startLineNumbers.length * this._lineHeight + state.lastLineRelativePosition; + const futureWidgetHeight = this._getHeightOfLines(state.startLineNumbers, state.lastLineRelativePosition); if (futureWidgetHeight > 0) { this._lastLineRelativePosition = state.lastLineRelativePosition; const lineNumbers = [...state.startLineNumbers]; @@ -228,18 +228,21 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._setHeight(0); return; } + let top: number = 0; // For existing sticky lines update the top and z-index for (const stickyLine of this._renderedStickyLines) { - this._updatePosition(stickyLine); + this._updatePosition(stickyLine, top); + top += stickyLine.height; } // For new sticky lines const layoutInfo = this._editor.getLayoutInfo(); const linesToRender = this._lineNumbers.slice(rebuildFromLine); for (const [index, line] of linesToRender.entries()) { - const stickyLine = this._renderChildNode(index + rebuildFromLine, line, foldingModel, layoutInfo); + const stickyLine = this._renderChildNode(index + rebuildFromLine, line, top, foldingModel, layoutInfo); if (!stickyLine) { continue; } + top += stickyLine.height; this._linesDomNode.appendChild(stickyLine.lineDomNode); this._lineNumbersDomNode.appendChild(stickyLine.lineNumberDomNode); this._renderedStickyLines.push(stickyLine); @@ -249,7 +252,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._useFoldingOpacityTransition(!this._isOnGlyphMargin); } - const widgetHeight = this._lineNumbers.length * this._lineHeight + this._lastLineRelativePosition; + const widgetHeight = top + this._lastLineRelativePosition; this._setHeight(widgetHeight); this._rootDomNode.style.marginLeft = '0px'; @@ -257,6 +260,14 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._editor.layoutOverlayWidget(this); } + private _getHeightOfLines(lineNumbers: number[], lastLineRelativePosition: number): number { + let totalHeight = 0; + for (let i = 0; i < lineNumbers.length; i++) { + totalHeight += this._editor.getLineHeightForLineNumber(lineNumbers[i]); + } + return totalHeight + lastLineRelativePosition; + } + private _setHeight(height: number): void { if (this._height === height) { return; @@ -291,7 +302,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { })); } - private _renderChildNode(index: number, line: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { + private _renderChildNode(index: number, line: number, top: number, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine | undefined { const viewModel = this._editor._getViewModel(); if (!viewModel) { return; @@ -307,7 +318,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { actualInlineDecorations = []; } - const lineHeight = this._lineHeight; + const lineHeight = this._editor.getLineHeightForLineNumber(line); const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, lineRenderingData.continuesWithWrappedLine, lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, @@ -359,6 +370,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { if (foldingIcon) { lineNumberHTMLNode.appendChild(foldingIcon.domNode); foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`; + foldingIcon.domNode.style.lineHeight = `${lineHeight}px`; } this._editor.applyFontInfo(lineHTMLNode); @@ -371,10 +383,10 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { lineHTMLNode.style.height = `${lineHeight}px`; const renderedLine = new RenderedStickyLine(index, line, lineHTMLNode, lineNumberHTMLNode, foldingIcon, renderOutput.characterMapping, lineHTMLNode.scrollWidth, lineHeight); - return this._updatePosition(renderedLine); + return this._updatePosition(renderedLine, top); } - private _updatePosition(stickyLine: RenderedStickyLine): RenderedStickyLine { + private _updatePosition(stickyLine: RenderedStickyLine, top: number): RenderedStickyLine { const index = stickyLine.index; const lineHTMLNode = stickyLine.lineDomNode; const lineNumberHTMLNode = stickyLine.lineNumberDomNode; @@ -383,16 +395,15 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const zIndex = '0'; lineHTMLNode.style.zIndex = zIndex; lineNumberHTMLNode.style.zIndex = zIndex; - const top = `${index * this._lineHeight + this._lastLineRelativePosition + (stickyLine.foldingIcon?.isCollapsed ? 1 : 0)}px`; - lineHTMLNode.style.top = top; - lineNumberHTMLNode.style.top = top; + const updatedTop = `${top + this._lastLineRelativePosition + (stickyLine.foldingIcon?.isCollapsed ? 1 : 0)}px`; + lineHTMLNode.style.top = updatedTop; + lineNumberHTMLNode.style.top = updatedTop; } else { const zIndex = '1'; lineHTMLNode.style.zIndex = zIndex; lineNumberHTMLNode.style.zIndex = zIndex; - const top = `${index * this._lineHeight}px`; - lineHTMLNode.style.top = top; - lineNumberHTMLNode.style.top = top; + lineHTMLNode.style.top = `${top}px`; + lineNumberHTMLNode.style.top = `${top}px`; } return stickyLine; } diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts index 07493f90714..918e06d8aeb 100644 --- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts +++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts @@ -150,7 +150,7 @@ suite('Sticky Scroll Tests', () => { await provider.update(); assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 1, endLineNumber: 4 }), [new StickyLineCandidate(1, 2, 0, 19)]); assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 8, endLineNumber: 10 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19)]); - assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 10, endLineNumber: 13 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19)]); + assert.deepStrictEqual(provider.getCandidateStickyLinesIntersecting({ startLineNumber: 10, endLineNumber: 13 }), [new StickyLineCandidate(7, 11, 0, 19), new StickyLineCandidate(9, 11, 19, 19), new StickyLineCandidate(10, 10, 38, 19), new StickyLineCandidate(13, 13, 0, 19)]); provider.dispose(); model.dispose(); diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index ddf887c8b2b..c3208cb7b76 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -42,7 +42,7 @@ import { IKeybindingItem, KeybindingsRegistry } from '../../../platform/keybindi import { ResolvedKeybindingItem } from '../../../platform/keybinding/common/resolvedKeybindingItem.js'; import { USLayoutResolvedKeybinding } from '../../../platform/keybinding/common/usLayoutResolvedKeybinding.js'; import { ILabelService, ResourceLabelFormatter, IFormatterChangeEvent, Verbosity } from '../../../platform/label/common/label.js'; -import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from '../../../platform/notification/common/notification.js'; +import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter, IStatusHandle } from '../../../platform/notification/common/notification.js'; import { IProgressRunner, IEditorProgressService, IProgressService, IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from '../../../platform/progress/common/progress.js'; import { ITelemetryService, TelemetryLevel } from '../../../platform/telemetry/common/telemetry.js'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, WorkbenchState, WorkspaceFolder, STANDALONE_EDITOR_WORKSPACE_ID } from '../../../platform/workspace/common/workspace.js'; @@ -350,11 +350,10 @@ export class StandaloneNotificationService implements INotificationService { return StandaloneNotificationService.NO_OP; } - public status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + public status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } - public setFilter(filter: NotificationsFilter | INotificationSourceFilter): void { } public getFilter(source?: INotificationSource): NotificationsFilter { diff --git a/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts b/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts new file mode 100644 index 00000000000..cc2e893d95a --- /dev/null +++ b/src/vs/editor/test/common/codecs/frontMatterDecoder.test.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../common/core/range.js'; +import { TestDecoder } from '../utils/testDecoder.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { newWriteableStream } from '../../../../base/common/stream.js'; +import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { DoubleQuote } from '../../../common/codecs/simpleCodec/tokens/doubleQuote.js'; +import { type TSimpleDecoderToken } from '../../../common/codecs/simpleCodec/simpleDecoder.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { FrontMatterDecoder } from '../../../common/codecs/frontMatterCodec/frontMatterDecoder.js'; +import { ExclamationMark, Quote, Tab, Word, Space, Colon } from '../../../common/codecs/simpleCodec/tokens/index.js'; +import { FrontMatterBoolean, FrontMatterString, FrontMatterArray, FrontMatterRecord, FrontMatterRecordDelimiter, FrontMatterRecordName } from '../../../common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Front Matter decoder for testing purposes. + */ +export class TestFrontMatterDecoder extends TestDecoder { + constructor() { + const stream = newWriteableStream(null); + const decoder = new FrontMatterDecoder(stream); + + super(stream, decoder); + } +} + +suite('FrontMatterDecoder', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('• produces expected tokens', async () => { + const test = disposables.add( + new TestFrontMatterDecoder(), + ); + + await test.run( + [ + 'just: "write some yaml "', + 'write-some :\t[ \' just\t \', "yaml!", true, , ,]', + 'anotherField \t\t\t : FALSE ', + ], + [ + // first record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(1, 1, 1, 1 + 4), 'just'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(1, 5, 1, 6)), + new Space(new Range(1, 6, 1, 7)), + ]), + new FrontMatterString([ + new DoubleQuote(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 8 + 5), 'write'), + new Space(new Range(1, 13, 1, 14)), + new Word(new Range(1, 14, 1, 14 + 4), 'some'), + new Space(new Range(1, 18, 1, 19)), + new Word(new Range(1, 19, 1, 19 + 4), 'yaml'), + new Space(new Range(1, 23, 1, 24)), + new DoubleQuote(new Range(1, 24, 1, 25)), + ]), + ]), + new NewLine(new Range(1, 25, 1, 26)), + // second record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(2, 1, 2, 1 + 10), 'write-some'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(2, 12, 2, 13)), + new Tab(new Range(2, 13, 2, 14)), + ]), + new FrontMatterArray([ + new LeftBracket(new Range(2, 14, 2, 15)), + new FrontMatterString([ + new Quote(new Range(2, 16, 2, 17)), + new Space(new Range(2, 17, 2, 18)), + new Word(new Range(2, 18, 2, 18 + 4), 'just'), + new Tab(new Range(2, 22, 2, 23)), + new Space(new Range(2, 23, 2, 24)), + new Quote(new Range(2, 24, 2, 25)), + ]), + new FrontMatterString([ + new DoubleQuote(new Range(2, 28, 2, 29)), + new Word(new Range(2, 29, 2, 29 + 4), 'yaml'), + new ExclamationMark(new Range(2, 33, 2, 34)), + new DoubleQuote(new Range(2, 34, 2, 35)), + ]), + new FrontMatterBoolean( + new Range(2, 37, 2, 37 + 4), + true, + ), + new RightBracket(new Range(2, 46, 2, 47)), + ]), + ]), + new NewLine(new Range(2, 47, 2, 48)), + // third record + new FrontMatterRecord([ + new FrontMatterRecordName([ + new Word(new Range(3, 1, 3, 1 + 12), 'anotherField'), + ]), + new FrontMatterRecordDelimiter([ + new Colon(new Range(3, 19, 3, 20)), + new Space(new Range(3, 20, 3, 21)), + ]), + new FrontMatterBoolean( + new Range(3, 22, 3, 22 + 5), + false, + ), + ]), + new Space(new Range(3, 27, 3, 28)), + ]); + }); +}); diff --git a/src/vs/editor/test/common/codecs/simpleDecoder.test.ts b/src/vs/editor/test/common/codecs/simpleDecoder.test.ts index d2af1e3adf6..7ebb92e68a0 100644 --- a/src/vs/editor/test/common/codecs/simpleDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/simpleDecoder.test.ts @@ -20,8 +20,10 @@ import { Colon, Slash, Space, + Quote, FormFeed, DollarSign, + DoubleQuote, VerticalTab, LeftBracket, RightBracket, @@ -32,6 +34,7 @@ import { RightParenthesis, LeftAngleBracket, RightAngleBracket, + Comma, } from '../../../common/codecs/simpleCodec/tokens/index.js'; /** @@ -76,13 +79,13 @@ suite('SimpleDecoder', () => { await test.run( [ ' hello world!', - 'how are\t you?\v', + 'how are\t "you?"\v', '', - ' (test) [!@#$:%^🦄&*_+=-]\f ', + ' (test) [!@#$:%^🦄&*_+=,-,]\f ', '\t\t🤗❤ \t', ' hey\v-\tthere\r', ' @workspace@legomushroom', - 'my ${text} /run', + '\'my\' ${text} /run', ], [ // first line @@ -98,9 +101,11 @@ suite('SimpleDecoder', () => { new Word(new Range(2, 5, 2, 8), 'are'), new Tab(new Range(2, 8, 2, 9)), new Space(new Range(2, 9, 2, 10)), - new Word(new Range(2, 10, 2, 14), 'you?'), - new VerticalTab(new Range(2, 14, 2, 15)), - new NewLine(new Range(2, 15, 2, 16)), + new DoubleQuote(new Range(2, 10, 2, 11)), + new Word(new Range(2, 11, 2, 11 + 4), 'you?'), + new DoubleQuote(new Range(2, 15, 2, 16)), + new VerticalTab(new Range(2, 16, 2, 17)), + new NewLine(new Range(2, 17, 2, 18)), // third line new NewLine(new Range(3, 1, 3, 2)), // fourth line @@ -119,12 +124,14 @@ suite('SimpleDecoder', () => { new DollarSign(new Range(4, 16, 4, 17)), new Colon(new Range(4, 17, 4, 18)), new Word(new Range(4, 18, 4, 18 + 9), '%^🦄&*_+='), - new Dash(new Range(4, 27, 4, 28)), - new RightBracket(new Range(4, 28, 4, 29)), - new FormFeed(new Range(4, 29, 4, 30)), - new Space(new Range(4, 30, 4, 31)), - new Space(new Range(4, 31, 4, 32)), - new NewLine(new Range(4, 32, 4, 33)), + new Comma(new Range(4, 27, 4, 28)), + new Dash(new Range(4, 28, 4, 29)), + new Comma(new Range(4, 29, 4, 30)), + new RightBracket(new Range(4, 30, 4, 31)), + new FormFeed(new Range(4, 31, 4, 32)), + new Space(new Range(4, 32, 4, 33)), + new Space(new Range(4, 33, 4, 34)), + new NewLine(new Range(4, 34, 4, 35)), // fifth line new Tab(new Range(5, 1, 5, 2)), new LeftAngleBracket(new Range(5, 2, 5, 3)), @@ -154,15 +161,17 @@ suite('SimpleDecoder', () => { new Word(new Range(7, 13, 7, 25), 'legomushroom'), new NewLine(new Range(7, 25, 7, 26)), // eighth line - new Word(new Range(8, 1, 8, 3), 'my'), - new Space(new Range(8, 3, 8, 4)), - new DollarSign(new Range(8, 4, 8, 5)), - new LeftCurlyBrace(new Range(8, 5, 8, 6)), - new Word(new Range(8, 6, 8, 6 + 4), 'text'), - new RightCurlyBrace(new Range(8, 10, 8, 11)), - new Space(new Range(8, 11, 8, 12)), - new Slash(new Range(8, 12, 8, 13)), - new Word(new Range(8, 13, 8, 13 + 3), 'run'), + new Quote(new Range(8, 1, 8, 2)), + new Word(new Range(8, 2, 8, 2 + 2), 'my'), + new Quote(new Range(8, 4, 8, 5)), + new Space(new Range(8, 5, 8, 6)), + new DollarSign(new Range(8, 6, 8, 7)), + new LeftCurlyBrace(new Range(8, 7, 8, 8)), + new Word(new Range(8, 8, 8, 8 + 4), 'text'), + new RightCurlyBrace(new Range(8, 12, 8, 13)), + new Space(new Range(8, 13, 8, 14)), + new Slash(new Range(8, 14, 8, 15)), + new Word(new Range(8, 15, 8, 15 + 3), 'run'), ], ); }); diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts new file mode 100644 index 00000000000..0047a4014ba --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { LineHeightsManager } from '../../../common/viewLayout/lineHeights.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; + +suite('Editor ViewLayout - LineHeightsManager', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('default line height is used when no custom heights exist', () => { + const manager = new LineHeightsManager(10, []); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(5), 10); + assert.strictEqual(manager.heightForLineNumber(100), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 50); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(10), 100); + }); + + test('can change default line height', () => { + const manager = new LineHeightsManager(10, []); + manager.defaultLineHeight = 20; + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 20); + assert.strictEqual(manager.heightForLineNumber(5), 20); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 100); + }); + + test('can add single custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 10); + assert.strictEqual(manager.heightForLineNumber(3), 20); + assert.strictEqual(manager.heightForLineNumber(4), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 40); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 50); + }); + + test('can add multiple custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 15); + manager.insertOrChangeCustomLineHeight('dec2', 4, 4, 25); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 15); + assert.strictEqual(manager.heightForLineNumber(3), 10); + assert.strictEqual(manager.heightForLineNumber(4), 25); + assert.strictEqual(manager.heightForLineNumber(5), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 25); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 35); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 60); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 70); + }); + + test('can add range of custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 2, 4, 15); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(1), 10); + assert.strictEqual(manager.heightForLineNumber(2), 15); + assert.strictEqual(manager.heightForLineNumber(3), 15); + assert.strictEqual(manager.heightForLineNumber(4), 15); + assert.strictEqual(manager.heightForLineNumber(5), 10); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 25); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 40); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 55); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 65); + }); + + test('can change existing custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 20); + + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 30); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 30); + + // Check accumulated heights after change + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 50); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 60); + }); + + test('can remove custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 20); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 20); + + manager.removeCustomLineHeight('dec1'); + manager.commit(); + assert.strictEqual(manager.heightForLineNumber(3), 10); + + // Check accumulated heights after removal + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 30); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 40); + }); + + test('handles overlapping custom line heights (last one wins)', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 5, 20); + manager.insertOrChangeCustomLineHeight('dec2', 4, 6, 30); + manager.commit(); + + assert.strictEqual(manager.heightForLineNumber(2), 10); + assert.strictEqual(manager.heightForLineNumber(3), 20); + assert.strictEqual(manager.heightForLineNumber(4), 30); + assert.strictEqual(manager.heightForLineNumber(5), 30); + assert.strictEqual(manager.heightForLineNumber(6), 30); + assert.strictEqual(manager.heightForLineNumber(7), 10); + }); + + test('handles deleting lines before custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 10, 12, 20); + manager.commit(); + + manager.onLinesDeleted(5, 7); // Delete lines 5-7 + + assert.strictEqual(manager.heightForLineNumber(7), 20); // Was line 10 + assert.strictEqual(manager.heightForLineNumber(8), 20); // Was line 11 + assert.strictEqual(manager.heightForLineNumber(9), 20); // Was line 12 + assert.strictEqual(manager.heightForLineNumber(10), 10); + }); + + test('handles deleting lines overlapping with custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 10, 20); + manager.commit(); + + manager.onLinesDeleted(7, 12); // Delete lines 7-12, including part of decoration + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 10); + }); + + test('handles deleting lines containing custom line heights completely', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesDeleted(4, 8); // Delete lines 4-8, completely contains decoration + + // The decoration collapses to a single line which matches the behavior in the text buffer + assert.strictEqual(manager.heightForLineNumber(3), 10); + assert.strictEqual(manager.heightForLineNumber(4), 20); + assert.strictEqual(manager.heightForLineNumber(5), 10); + }); + + test('handles inserting lines before custom line heights', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesInserted(3, 4); // Insert 2 lines at line 3 + + assert.strictEqual(manager.heightForLineNumber(5), 10); + assert.strictEqual(manager.heightForLineNumber(6), 10); + assert.strictEqual(manager.heightForLineNumber(7), 20); // Was line 5 + assert.strictEqual(manager.heightForLineNumber(8), 20); // Was line 6 + assert.strictEqual(manager.heightForLineNumber(9), 20); // Was line 7 + }); + + test('handles inserting lines inside custom line heights range', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.onLinesInserted(6, 7); // Insert 2 lines at line 6 + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 20); + assert.strictEqual(manager.heightForLineNumber(8), 20); + assert.strictEqual(manager.heightForLineNumber(9), 20); + }); + + test('changing decoration id maintains custom line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); + manager.commit(); + + manager.removeCustomLineHeight('dec1'); + manager.insertOrChangeCustomLineHeight('dec2', 5, 7, 20); + manager.commit(); + + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 20); + assert.strictEqual(manager.heightForLineNumber(7), 20); + }); + + test('accumulates heights correctly with complex setup', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('dec1', 3, 3, 15); + manager.insertOrChangeCustomLineHeight('dec2', 5, 7, 20); + manager.insertOrChangeCustomLineHeight('dec3', 10, 10, 30); + manager.commit(); + + // Check accumulated heights + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(1), 10); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(2), 20); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(3), 35); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(4), 45); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(5), 65); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(7), 105); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(9), 125); + assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(10), 155); + }); + + test('partial deletion with multiple lines for the same decoration ID', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('decSame', 5, 5, 20); + manager.insertOrChangeCustomLineHeight('decSame', 6, 6, 25); + manager.commit(); + + // Delete one line that partially intersects the same decoration + manager.onLinesDeleted(6, 6); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(5), 20); + assert.strictEqual(manager.heightForLineNumber(6), 10); + }); + + test('overlapping decorations use maximum line height', () => { + const manager = new LineHeightsManager(10, []); + manager.insertOrChangeCustomLineHeight('decA', 3, 5, 40); + manager.insertOrChangeCustomLineHeight('decB', 4, 6, 30); + manager.commit(); + + // Check individual line heights + assert.strictEqual(manager.heightForLineNumber(3), 40); + assert.strictEqual(manager.heightForLineNumber(4), 40); + assert.strictEqual(manager.heightForLineNumber(5), 40); + assert.strictEqual(manager.heightForLineNumber(6), 30); + }); +}); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 087c24e457b..7bf20a78d84 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -33,7 +33,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout 1', () => { // Start off with 10 lines - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - @@ -142,7 +142,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout 2', () => { // Start off with 10 lines and one whitespace after line 2, of height 5 - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); const a = insertWhitespace(linesLayout, 2, 0, 5, 0); // 10 lines @@ -239,7 +239,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout Padding', () => { // Start off with 10 lines - const linesLayout = new LinesLayout(10, 10, 15, 20); + const linesLayout = new LinesLayout(10, 10, 15, 20, []); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - @@ -333,7 +333,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines @@ -382,7 +382,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getCenteredLineInViewport', () => { - const linesLayout = new LinesLayout(10, 1, 0, 0); + const linesLayout = new LinesLayout(10, 1, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines @@ -465,7 +465,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLinesViewportData 1', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); insertWhitespace(linesLayout, 6, 0, 100, 0); // 10 lines @@ -598,7 +598,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); const a = insertWhitespace(linesLayout, 6, 0, 100, 0); const b = insertWhitespace(linesLayout, 7, 0, 50, 0); @@ -669,7 +669,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout getWhitespaceAtVerticalOffset', () => { - const linesLayout = new LinesLayout(10, 10, 0, 0); + const linesLayout = new LinesLayout(10, 10, 0, 0, []); const a = insertWhitespace(linesLayout, 6, 0, 100, 0); const b = insertWhitespace(linesLayout, 7, 0, 50, 0); @@ -712,7 +712,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); // Insert a whitespace after line number 2, of height 10 const a = insertWhitespace(linesLayout, 2, 0, 10, 0); @@ -1063,7 +1063,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); @@ -1187,7 +1187,7 @@ suite('Editor ViewLayout - LinesLayout', () => { }); test('LinesLayout Bug', () => { - const linesLayout = new LinesLayout(100, 20, 0, 0); + const linesLayout = new LinesLayout(100, 20, 0, 0, []); const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f609f7a146e..a5ce6e74f66 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -518,6 +518,7 @@ declare namespace monaco { readonly altKey: boolean; readonly metaKey: boolean; readonly timestamp: number; + readonly defaultPrevented: boolean; preventDefault(): void; stopPropagation(): void; } @@ -1743,6 +1744,10 @@ declare namespace monaco.editor { * with the specified {@link IModelDecorationGlyphMarginOptions} in the glyph margin. */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; + /** + * If set, the decoration will override the line height of the lines it spans. + */ + lineHeight?: number | null; /** * If set, the decoration will be rendered in the lines decorations with this CSS class name. */ @@ -2277,6 +2282,11 @@ declare namespace monaco.editor { * @param ownerId If set, it will ignore decorations belonging to other owners. */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ @@ -6099,6 +6109,10 @@ declare namespace monaco.editor { * Get the vertical position (top offset) for the position w.r.t. to the first line. */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the line height for the line number. + */ + getLineHeightForLineNumber(lineNumber: number): number; /** * Write the screen reader content to be the current selection */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 009c85db29a..128c3fe2064 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -318,6 +318,8 @@ export class Sound { public static readonly chatEditModifiedFile = Sound.register({ fileName: 'chatEditModifiedFile.mp3' }); public static readonly editsKept = Sound.register({ fileName: 'editsKept.mp3' }); public static readonly editsUndone = Sound.register({ fileName: 'editsUndone.mp3' }); + public static readonly nextEditSuggestion = Sound.register({ fileName: 'nextEditSuggestion.mp3' }); + public static readonly terminalCommandSucceeded = Sound.register({ fileName: 'terminalCommandSucceeded.mp3' }); private constructor(public readonly fileName: string) { } } @@ -434,7 +436,13 @@ export class AccessibilitySignal { legacySoundSettingsKey: 'audioCues.lineHasInlineSuggestion', settingsKey: 'accessibility.signals.lineHasInlineSuggestion', }); - + public static readonly nextEditSuggestion = AccessibilitySignal.register({ + name: localize('accessibilitySignals.nextEditSuggestion.name', 'Next Edit Suggestion on Line'), + sound: Sound.nextEditSuggestion, + legacySoundSettingsKey: 'audioCues.nextEditSuggestion', + settingsKey: 'accessibility.signals.nextEditSuggestion', + announcementMessage: localize('accessibility.signals.nextEditSuggestion', 'Next Edit Suggestion'), + }); public static readonly terminalQuickFix = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalQuickFix.name', 'Terminal Quick Fix'), sound: Sound.quickFixes, @@ -491,7 +499,7 @@ export class AccessibilitySignal { public static readonly terminalCommandSucceeded = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'), - sound: Sound.success, + sound: Sound.terminalCommandSucceeded, announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'), settingsKey: 'accessibility.signals.terminalCommandSucceeded', }); diff --git a/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 b/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 new file mode 100644 index 00000000000..ae39e3aaf41 Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/nextEditSuggestion.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/media/save.mp3 b/src/vs/platform/accessibilitySignal/browser/media/save.mp3 index 68a9cc83565..147f81af550 100644 Binary files a/src/vs/platform/accessibilitySignal/browser/media/save.mp3 and b/src/vs/platform/accessibilitySignal/browser/media/save.mp3 differ diff --git a/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 b/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 new file mode 100644 index 00000000000..68a9cc83565 Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/terminalCommandSucceeded.mp3 differ diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index ff973d299fe..3f6a3760f8c 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -36,15 +36,18 @@ export interface IActionListItem { readonly group?: { kind?: any; icon?: ThemeIcon; title: string }; readonly disabled?: boolean; readonly label?: string; + readonly description?: string; readonly keybinding?: ResolvedKeybinding; canPreview?: boolean | undefined; readonly hideIcon?: boolean; + readonly tooltip?: string; } interface IActionMenuTemplateData { readonly container: HTMLElement; readonly icon: HTMLElement; readonly text: HTMLElement; + readonly description?: HTMLElement; readonly keybinding: KeybindingLabel; } @@ -100,9 +103,13 @@ class ActionItemRenderer implements IListRenderer, IAction text.className = 'title'; container.append(text); + const description = document.createElement('span'); + description.className = 'description'; + container.append(description); + const keybinding = new KeybindingLabel(container, OS); - return { container, icon, text, keybinding }; + return { container, icon, text, description, keybinding }; } renderElement(element: IActionListItem, _index: number, data: IActionMenuTemplateData): void { @@ -124,13 +131,23 @@ class ActionItemRenderer implements IListRenderer, IAction data.text.textContent = stripNewlines(element.label); + if (element.description) { + data.description!.textContent = stripNewlines(element.description); + data.description!.style.display = 'inline'; + } else { + data.description!.textContent = ''; + data.description!.style.display = 'none'; + } + data.keybinding.set(element.keybinding); dom.setVisibility(!!element.keybinding, data.keybinding.element); const actionTitle = this._keybindingService.lookupKeybinding(acceptSelectedActionCommand)?.getLabel(); const previewTitle = this._keybindingService.lookupKeybinding(previewSelectedActionCommand)?.getLabel(); data.container.classList.toggle('option-disabled', element.disabled); - if (element.disabled) { + if (element.tooltip) { + data.container.title = element.tooltip; + } else if (element.disabled) { data.container.title = element.label; } else if (actionTitle && previewTitle) { if (this._supportsPreview && element.canPreview) { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index d205c7ab791..9f454528d91 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -168,3 +168,9 @@ /* The important gives this rule precedence over the hover rule. */ background: var(--vscode-actionBar-toggledBackground) !important; } + +.action-widget .monaco-list .monaco-list-row .description { + opacity: 0.7; + margin-left: 0.5em; + font-size: 0.9em; +} diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index 450126a0f27..43179ddc230 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -18,6 +18,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common import { createDecorator, IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js'; import { inputActiveOptionBackground, registerColor } from '../../theme/common/colorRegistry.js'; +import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js'; registerColor( 'actionBar.toggledBackground', @@ -34,7 +35,7 @@ export const IActionWidgetService = createDecorator('actio export interface IActionWidgetService { readonly _serviceBrand: undefined; - show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void; + show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void; hide(didCancel?: boolean): void; @@ -58,7 +59,7 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { super(); } - show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void { + show(user: string, supportsPreview: boolean, items: readonly IActionListItem[], delegate: IActionListDelegate, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[]): void { const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService); const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate); diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index e477d3ddbdd..3200306b821 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -85,7 +85,13 @@ export class WorkbenchButtonBar extends ButtonBar { const actionOrSubmenu = actions[i]; let action: IAction; let btn: IButton; - + let tooltip: string = ''; + const kb = this._keybindingService.lookupKeybinding(actionOrSubmenu.id); + if (kb) { + tooltip = localize('labelWithKeybinding', "{0} ({1})", actionOrSubmenu.tooltip || actionOrSubmenu.label, kb.getLabel()); + } else { + tooltip = actionOrSubmenu.tooltip || actionOrSubmenu.label; + } if (actionOrSubmenu instanceof SubmenuAction && actionOrSubmenu.actions.length > 0) { const [first, ...rest] = actionOrSubmenu.actions; action = first; @@ -94,14 +100,14 @@ export class WorkbenchButtonBar extends ButtonBar { actionRunner: this._actionRunner, actions: rest, contextMenuProvider: this._contextMenuService, - ariaLabel: action.label, + ariaLabel: tooltip, supportIcons: true, }); } else { action = actionOrSubmenu; btn = this.addButton({ secondary: conifgProvider(action, i)?.isSecondary ?? secondary, - ariaLabel: action.label, + ariaLabel: tooltip, supportIcons: true, }); } @@ -128,13 +134,7 @@ export class WorkbenchButtonBar extends ButtonBar { btn.element.classList.add(...action.class.split(' ')); } } - const kb = this._keybindingService.lookupKeybinding(action.id); - let tooltip: string; - if (kb) { - tooltip = localize('labelWithKeybinding', "{0} ({1})", action.tooltip || action.label, kb.getLabel()); - } else { - tooltip = action.tooltip || action.label; - } + this._updateStore.add(this._hoverService.setupManagedHover(hoverDelegate, btn.element, tooltip)); this._updateStore.add(btn.onDidClick(async () => { this._actionRunner.run(action); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index a205483c48c..7ea314341a3 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -104,7 +104,7 @@ export const OPTIONS: OptionDescriptions> = { 'update-extensions': { type: 'boolean', cat: 'e', description: localize('updateExtensions', "Update the installed extensions.") }, 'enable-proposed-api': { type: 'string[]', allowEmptyValue: true, cat: 'e', args: 'ext-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, - 'add-mcp': { type: 'string[]', cat: 'o', args: 'json', description: localize('addMcp', "Adds a Model Context Protocol server definition to the user profile, or workspace or folder when used with --mcp-workspace. Accepts JSON input in the form '{\"name\":\"server-name\",\"command\":...}'") }, + 'add-mcp': { type: 'string[]', cat: 'o', args: 'json', description: localize('addMcp', "Adds a Model Context Protocol server definition to the user profile. Accepts JSON input in the form '{\"name\":\"server-name\",\"command\":...}'") }, 'version': { type: 'boolean', cat: 't', alias: 'v', description: localize('version', "Print version.") }, 'verbose': { type: 'boolean', cat: 't', global: true, description: localize('verbose', "Print verbose output (implies --wait).") }, diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts b/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts index 14e529e97e1..0999bc27635 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts @@ -15,7 +15,7 @@ export const enum ExtensionGalleryResourceType { ExtensionDetailsViewUri = 'ExtensionDetailsViewUriTemplate', ExtensionRatingViewUri = 'ExtensionRatingViewUriTemplate', ExtensionResourceUri = 'ExtensionResourceUriTemplate', - ReportIssueUri = 'ReportIssueUri', + ContactSupportUri = 'ContactSupportUri', } export const enum Flag { @@ -55,7 +55,12 @@ export interface IExtensionGalleryManifest { readonly flags?: readonly ExtensionQueryCapabilityValue[]; }; readonly signing?: { - readonly allRepositorySigned: boolean; + readonly allPublicRepositorySigned: boolean; + readonly allPrivateRepositorySigned?: boolean; + }; + readonly extensions?: { + readonly includePublicExtensions?: boolean; + readonly includePrivateExtensions?: boolean; }; }; } @@ -70,10 +75,11 @@ export interface IExtensionGalleryManifestService { getExtensionGalleryManifest(): Promise; } -export function getExtensionGalleryManifestResourceUri(manifest: IExtensionGalleryManifest, type: ExtensionGalleryResourceType, version?: string): string | undefined { +export function getExtensionGalleryManifestResourceUri(manifest: IExtensionGalleryManifest, type: string): string | undefined { + const [name, version] = type.split('/'); for (const resource of manifest.resources) { const [r, v] = resource.type.split('/'); - if (r !== type) { + if (r !== name) { continue; } if (!version || v === version) { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts index 24013df1978..70e34e808be 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts @@ -223,7 +223,7 @@ export class ExtensionGalleryManifestService extends Disposable implements IExte flags, }, signing: { - allRepositorySigned: true, + allPublicRepositorySigned: true, } } }; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 7aa58e32255..7dbac890425 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -70,6 +70,7 @@ interface IRawGalleryExtensionPublisher { readonly publisherName: string; readonly domain?: string | null; readonly isDomainVerified?: boolean; + readonly linkType?: string; } interface IRawGalleryExtension { @@ -86,6 +87,8 @@ interface IRawGalleryExtension { readonly lastUpdated: string; readonly categories: string[] | undefined; readonly flags: string; + readonly linkType?: string; + readonly ratingLinkType?: string; } interface IRawGalleryExtensionsResult { @@ -465,9 +468,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller coreTranslations: getCoreTranslationAssets(version) }; - const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionDetailsViewUri); - const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.PublisherViewUri); - const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionRatingViewUri); + const detailsViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.linkType ?? ExtensionGalleryResourceType.ExtensionDetailsViewUri); + const publisherViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.publisher.linkType ?? ExtensionGalleryResourceType.PublisherViewUri); + const ratingViewUri = getExtensionGalleryManifestResourceUri(extensionGalleryManifest, galleryExtension.ratingLinkType ?? ExtensionGalleryResourceType.ExtensionRatingViewUri); const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName); return { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 769bb119440..af82383f60f 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -16,6 +16,7 @@ import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from '. import { FileOperationError, FileOperationResult, IFileService, IFileStat } from '../../files/common/files.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { Registry } from '../../registry/common/platform.js'; +import { IExtensionGalleryManifest } from './extensionGalleryManifest.js'; export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); @@ -777,3 +778,11 @@ Registry.as(Extensions.Configuration) } } }); + +export function shouldRequireRepositorySignatureFor(isPrivate: boolean, galleryManifest: IExtensionGalleryManifest | null): boolean { + if (isPrivate) { + return galleryManifest?.capabilities.signing?.allPrivateRepositorySigned === true; + } + return galleryManifest?.capabilities.signing?.allPublicRepositorySigned === true; +} + diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 16ba453fd05..4e920ab4ee2 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -177,7 +177,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable await this.withProfileExtensions(profileLocation, profileExtensions => { const result: IScannedProfileExtension[] = []; for (const profileExtension of profileExtensions) { - const extension = extensions.find(([e]) => areSameExtensions(e.identifier, profileExtension.identifier) && e.manifest.version === profileExtension.version); + const extension = extensions.find(([e]) => areSameExtensions({ id: e.identifier.id }, { id: profileExtension.identifier.id }) && e.manifest.version === profileExtension.version); if (extension) { profileExtension.metadata = { ...profileExtension.metadata, ...extension[1] }; updatedExtensions.push(profileExtension); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index cfd01c91ac2..0a50a2edaf4 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -35,6 +35,7 @@ import { computeSize, IAllowedExtensionsService, VerifyExtensionSignatureConfigKey, + shouldRequireRepositorySignatureFor, } from '../common/extensionManagement.js'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js'; @@ -334,7 +335,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi verifySignature = isBoolean(value) ? value : true; } const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform); - const shouldRequireSignature = (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned; + const shouldRequireSignature = shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest()); if ( verificationStatus !== ExtensionSignatureVerificationCode.Success diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index e21a27208f3..1636bf4db20 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -33,7 +33,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 7 + version: 8 }, chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', @@ -231,6 +231,7 @@ const _allApiProposals = { }, languageModelDataPart: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts', + version: 1 }, languageModelSystem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index f3e1e2b854d..ee26e7b40b9 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { IME } from '../../../base/common/ime.js'; import { KeyCode } from '../../../base/common/keyCodes.js'; import { Keybinding, ResolvedChord, ResolvedKeybinding, SingleModifierChord } from '../../../base/common/keybindings.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; import * as nls from '../../../nls.js'; import { ICommandService } from '../../commands/common/commands.js'; @@ -20,7 +20,7 @@ import { IKeybindingService, IKeyboardEvent, KeybindingsSchemaContribution } fro import { ResolutionResult, KeybindingResolver, ResultKind, NoMatchingKb } from './keybindingResolver.js'; import { ResolvedKeybindingItem } from './resolvedKeybindingItem.js'; import { ILogService } from '../../log/common/log.js'; -import { INotificationService } from '../../notification/common/notification.js'; +import { INotificationService, IStatusHandle } from '../../notification/common/notification.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; interface CurrentChord { @@ -49,7 +49,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChords: CurrentChord[]; private _currentChordChecker: IntervalTimer; - private _currentChordStatusMessage: IDisposable | null; + private _currentChordStatusMessage: IStatusHandle | null; private _ignoreSingleModifiers: KeybindingModifierSet; private _currentSingleModifier: SingleModifierChord | null; private _currentSingleModifierClearTimeout: TimeoutTimer; @@ -203,7 +203,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _leaveChordMode(): void { if (this._currentChordStatusMessage) { - this._currentChordStatusMessage.dispose(); + this._currentChordStatusMessage.close(); this._currentChordStatusMessage = null; } this._currentChordChecker.cancel(); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 4a5f4c83407..04af18c812b 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -184,7 +184,7 @@ suite('AbstractKeybindingService', () => { status(message: string, options?: IStatusMessageOptions) { statusMessageCalls!.push(message); return { - dispose: () => { + close: () => { statusMessageCallsDisposed!.push(message); } }; diff --git a/src/vs/platform/mcp/common/mcpManagementCli.ts b/src/vs/platform/mcp/common/mcpManagementCli.ts index 55106298512..cecdfaa07a4 100644 --- a/src/vs/platform/mcp/common/mcpManagementCli.ts +++ b/src/vs/platform/mcp/common/mcpManagementCli.ts @@ -5,9 +5,9 @@ import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ILogger } from '../../log/common/log.js'; -import { IMcpConfiguration, IMcpConfigurationSSE, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js'; +import { IMcpConfiguration, IMcpConfigurationHTTP, IMcpConfigurationStdio, McpConfigurationServer } from './mcpPlatformTypes.js'; -type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationSSE }; +type ValidatedConfig = { name: string; config: IMcpConfigurationStdio | IMcpConfigurationHTTP }; export class McpManagementCli { constructor( @@ -51,7 +51,7 @@ export class McpManagementCli { } const { name, ...rest } = parsed; - return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationSSE }; + return { name, config: rest as IMcpConfigurationStdio | IMcpConfigurationHTTP }; } } diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index b16d59e2934..078c70306cf 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -7,10 +7,10 @@ export interface IMcpConfiguration { inputs?: unknown[]; /** @deprecated Only for rough cross-compat with other formats */ mcpServers?: Record; - servers?: Record; + servers?: Record; } -export type McpConfigurationServer = IMcpConfigurationStdio | IMcpConfigurationSSE; +export type McpConfigurationServer = IMcpConfigurationStdio | IMcpConfigurationHTTP; export interface IMcpConfigurationStdio { type?: 'stdio'; @@ -20,8 +20,8 @@ export interface IMcpConfigurationStdio { envFile?: string; } -export interface IMcpConfigurationSSE { - type: 'sse'; +export interface IMcpConfigurationHTTP { + type?: 'http'; url: string; headers?: Record; } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 641ad809261..a3b59ab038e 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -5,7 +5,6 @@ import { IAction } from '../../../base/common/actions.js'; import { Event } from '../../../base/common/event.js'; -import { IDisposable } from '../../../base/common/lifecycle.js'; import BaseSeverity from '../../../base/common/severity.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; @@ -273,6 +272,14 @@ export interface INotificationHandle { close(): void; } +export interface IStatusHandle { + + /** + * Hide the status message. + */ + close(): void; +} + interface IBasePromptChoice { /** @@ -450,9 +457,9 @@ export interface INotificationService { * @param message the message to show as status * @param options provides some optional configuration options * - * @returns a disposable to hide the status message + * @returns a handle to hide the status message */ - status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle; } export class NoOpNotification implements INotificationHandle { diff --git a/src/vs/platform/notification/test/common/testNotificationService.ts b/src/vs/platform/notification/test/common/testNotificationService.ts index f016e186cf5..80def17f244 100644 --- a/src/vs/platform/notification/test/common/testNotificationService.ts +++ b/src/vs/platform/notification/test/common/testNotificationService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { INotification, INotificationHandle, INotificationService, INotificationSource, INotificationSourceFilter, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NotificationsFilter, Severity } from '../../common/notification.js'; +import { INotification, INotificationHandle, INotificationService, INotificationSource, INotificationSourceFilter, IPromptChoice, IPromptOptions, IStatusHandle, IStatusMessageOptions, NoOpNotification, NotificationsFilter, Severity } from '../../common/notification.js'; export class TestNotificationService implements INotificationService { @@ -39,8 +38,10 @@ export class TestNotificationService implements INotificationService { return TestNotificationService.NO_OP; } - status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { + close: () => { } + }; } setFilter(): void { } diff --git a/src/vs/platform/prompts/common/constants.ts b/src/vs/platform/prompts/common/constants.ts index d2def9b1ba8..594f6d538b3 100644 --- a/src/vs/platform/prompts/common/constants.ts +++ b/src/vs/platform/prompts/common/constants.ts @@ -11,6 +11,11 @@ import { basename } from '../../../base/common/path.js'; */ export const PROMPT_FILE_EXTENSION = '.prompt.md'; +/** + * File extension for the reusable instruction files. + */ +export const INSTRUCTION_FILE_EXTENSION = '.instructions.md'; + /** * Copilot custom instructions file name. */ @@ -33,18 +38,33 @@ export const LOCATIONS_CONFIG_KEY: string = 'chat.promptFilesLocations'; export const DEFAULT_SOURCE_FOLDER = '.github/prompts'; /** - * Check if provided URI points to a file that with prompt file extension. + * Gets the prompt file type from the provided path. */ -export const isPromptFile = ( - fileUri: URI, -): boolean => { +export function getPromptFileType(fileUri: URI): 'instructions' | 'prompt' | undefined { const filename = basename(fileUri.path); - const hasPromptFileExtension = filename.endsWith(PROMPT_FILE_EXTENSION); - const isCustomInstructionsFile = (filename === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); + if (filename.endsWith(PROMPT_FILE_EXTENSION)) { + return 'prompt'; + } - return hasPromptFileExtension || isCustomInstructionsFile; -}; + if (filename.endsWith(INSTRUCTION_FILE_EXTENSION) || (filename === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME)) { + return 'instructions'; + } + + return undefined; +} + +/** + * Check if provided URI points to a file that with prompt file extension. + */ +export function isPromptOrInstructionsFile(fileUri: URI): boolean { + return getPromptFileType(fileUri) !== undefined; +} + + +export function getPromptFileExtension(type: 'instructions' | 'prompt'): string { + return type === 'instructions' ? INSTRUCTION_FILE_EXTENSION : PROMPT_FILE_EXTENSION; +} /** * Check whether provided URI belongs to an `untitled` document. @@ -57,29 +77,27 @@ export const isUntitled = ( /** * Gets clean prompt name without file extension. - * - * @throws If provided path is not a prompt file - * (does not end with {@link PROMPT_FILE_EXTENSION}). */ export const getCleanPromptName = ( fileUri: URI, ): string => { - // if an untitled document, use it's `path` component as the name - if (isUntitled(fileUri)) { - return fileUri.path; + const fileName = basename(fileUri.path); + + if (fileName.endsWith(PROMPT_FILE_EXTENSION)) { + return basename(fileUri.path, PROMPT_FILE_EXTENSION); } - // any file can be a prompt file if user selects the "prompt" language in - // the editor, so in this case return the full file name with file extension - if (isPromptFile(fileUri) === false) { - return basename(fileUri.path); + if (fileName.endsWith(INSTRUCTION_FILE_EXTENSION)) { + return basename(fileUri.path, INSTRUCTION_FILE_EXTENSION); } - // if a Copilot custom instructions file, remove `markdown` file extension - // otherwise, remove the `prompt` file extension - const fileExtension = (fileUri.path.endsWith(COPILOT_CUSTOM_INSTRUCTIONS_FILENAME)) - ? '.md' - : PROMPT_FILE_EXTENSION; + if (fileName === COPILOT_CUSTOM_INSTRUCTIONS_FILENAME) { + return basename(fileUri.path, '.md'); + } - return basename(fileUri.path, fileExtension); + // because we now rely on the `prompt` language ID that can be explicitly + // set for any document in the editor, any file can be a "prompt" file, so + // to account for that, we return the full file name including the file + // extension for all other cases + return basename(fileUri.path); }; diff --git a/src/vs/platform/prompts/test/common/constants.test.ts b/src/vs/platform/prompts/test/common/constants.test.ts index c6dfd97f776..3dcd0927f98 100644 --- a/src/vs/platform/prompts/test/common/constants.test.ts +++ b/src/vs/platform/prompts/test/common/constants.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { randomInt } from '../../../../base/common/numbers.js'; -import { getCleanPromptName, isPromptFile } from '../../common/constants.js'; +import { getCleanPromptName, isPromptOrInstructionsFile } from '../../common/constants.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; @@ -53,36 +53,36 @@ suite('Prompt Constants', () => { }); }); - suite('• isPromptFile', () => { + suite('• isPromptOrInstructionsFile', () => { test('• returns `true` for prompt files', () => { assert( - isPromptFile(URI.file('/path/to/my-prompt.prompt.md')), + isPromptOrInstructionsFile(URI.file('/path/to/my-prompt.prompt.md')), ); assert( - isPromptFile(URI.file('../common.prompt.md')), + isPromptOrInstructionsFile(URI.file('../common.prompt.md')), ); assert( - isPromptFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)), + isPromptOrInstructionsFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)), ); assert( - isPromptFile(URI.file('.github/copilot-instructions.md')), + isPromptOrInstructionsFile(URI.file('.github/copilot-instructions.md')), ); }); test('• returns `false` for non-prompt files', () => { assert( - !isPromptFile(URI.file('/path/to/my-prompt.prompt.md1')), + !isPromptOrInstructionsFile(URI.file('/path/to/my-prompt.prompt.md1')), ); assert( - !isPromptFile(URI.file('../common.md')), + !isPromptOrInstructionsFile(URI.file('../common.md')), ); assert( - !isPromptFile(URI.file(`./some-${randomInt(1000)}.txt`)), + !isPromptOrInstructionsFile(URI.file(`./some-${randomInt(1000)}.txt`)), ); }); }); diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index e69e588b3fe..31eb809d758 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -167,7 +167,8 @@ } .quick-input-widget.hidden-input .quick-input-list { - margin-top: 4px; /* reduce margins when input box hidden */ + margin-top: 4px; + /* reduce margins when input box hidden */ padding-bottom: 4px; } @@ -212,9 +213,9 @@ flex: 1; } -.quick-input-list .quick-input-list-checkbox { +.quick-input-widget .monaco-checkbox { + margin-right: 0; align-self: center; - margin: 0; } .quick-input-list .quick-input-list-icon { @@ -239,24 +240,6 @@ margin-left: 5px; } -.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows { - margin-left: 10px; -} - -.quick-input-widget .quick-input-list .quick-input-list-checkbox { - display: none; -} -.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox { - display: inline; -} -.quick-input-widget.show-checkboxes .quick-input-list-entry.not-pickable { - margin-left: -10px; - - .quick-input-list-checkbox { - display: none; - } -} - .quick-input-list .quick-input-list-rows > .quick-input-list-row { display: flex; align-items: center; @@ -264,7 +247,8 @@ .quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label, .quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container { - flex: 1; /* make sure the icon label grows within the row */ + flex: 1; + /* make sure the icon label grows within the row */ } .quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon[class*='codicon-'] { @@ -276,7 +260,8 @@ } .quick-input-list .quick-input-list-entry .quick-input-list-entry-keybinding { - margin-right: 8px; /* separate from the separator label or scrollbar if any */ + margin-right: 8px; + /* separate from the separator label or scrollbar if any */ } .quick-input-list .quick-input-list-label-meta { @@ -299,7 +284,8 @@ } .quick-input-list .quick-input-list-entry .quick-input-list-separator { - margin-right: 4px; /* separate from keybindings or actions */ + margin-right: 4px; + /* separate from keybindings or actions */ } .quick-input-list .quick-input-list-entry-action-bar { @@ -326,7 +312,8 @@ } .quick-input-list .quick-input-list-entry-action-bar { - margin-right: 4px; /* separate from scrollbar */ + margin-right: 4px; + /* separate from scrollbar */ } .quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .action-label.always-visible, @@ -342,6 +329,7 @@ .quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator { color: inherit } + .quick-input-list .monaco-list-row.focused .monaco-keybinding-key { background: none; } diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index e55ae787e61..60370bfdee3 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -13,7 +13,7 @@ import { IInputBoxStyles } from '../../../base/browser/ui/inputbox/inputBox.js'; import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { IListStyles } from '../../../base/browser/ui/list/listWidget.js'; import { IProgressBarStyles, ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js'; -import { IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; +import { Checkbox, IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js'; import { equals } from '../../../base/common/arrays.js'; import { TimeoutTimer } from '../../../base/common/async.js'; import { Codicon } from '../../../base/common/codicons.js'; @@ -103,7 +103,7 @@ export interface QuickInputUI { widget: HTMLElement; rightActionBar: ActionBar; inlineActionBar: ActionBar; - checkAll: HTMLInputElement; + checkAll: Checkbox; inputContainer: HTMLElement; filterContainer: HTMLElement; inputBox: QuickInputBox; @@ -1057,6 +1057,9 @@ export class QuickPickdom.append(headerContainer, $('input.quick-input-check-all')); - checkAll.type = 'checkbox'; - checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); - this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { + const checkAll = this._register(new Checkbox(localize('quickInput.checkAll', "Toggle all checkboxes"), false, { ...defaultCheckboxStyles, size: 15 })); + dom.append(headerContainer, checkAll.domNode); + this._register(checkAll.onChange(() => { const checked = checkAll.checked; list.setAllVisibleChecked(checked); })); - this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { + this._register(dom.addDisposableListener(checkAll.domNode, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space'... inputBox.setFocus(); } @@ -688,7 +689,7 @@ export class QuickInputController extends Disposable { ui.title.style.display = visibilities.title ? '' : 'none'; ui.description1.style.display = visibilities.description && (visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; ui.description2.style.display = visibilities.description && !(visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; - ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; + ui.checkAll.domNode.style.display = visibilities.checkAll ? '' : 'none'; ui.inputContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; @@ -706,16 +707,21 @@ export class QuickInputController extends Disposable { private setEnabled(enabled: boolean) { if (enabled !== this.enabled) { this.enabled = enabled; - for (const item of this.getUI().leftActionBar.viewItems) { + const ui = this.getUI(); + for (const item of ui.leftActionBar.viewItems) { (item as ActionViewItem).action.enabled = enabled; } - for (const item of this.getUI().rightActionBar.viewItems) { + for (const item of ui.rightActionBar.viewItems) { (item as ActionViewItem).action.enabled = enabled; } - this.getUI().checkAll.disabled = !enabled; - this.getUI().inputBox.enabled = enabled; - this.getUI().ok.enabled = enabled; - this.getUI().list.enabled = enabled; + if (enabled) { + ui.checkAll.enable(); + } else { + ui.checkAll.disable(); + } + ui.inputBox.enabled = enabled; + ui.ok.enabled = enabled; + ui.list.enabled = enabled; } } diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 15c5eef04cc..bce6f79c693 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -3,44 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from '../../../base/browser/dom.js'; import * as cssJs from '../../../base/browser/cssValue.js'; -import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from '../../../base/common/event.js'; -import { IHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate.js'; -import { IListVirtualDelegate } from '../../../base/browser/ui/list/list.js'; -import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; -import { localize } from '../../../nls.js'; -import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { WorkbenchObjectTree } from '../../list/browser/listService.js'; -import { IThemeService } from '../../theme/common/themeService.js'; -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem, QuickPickFocus } from '../common/quickInput.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { IMatch } from '../../../base/common/filters.js'; -import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; -import { AriaRole } from '../../../base/browser/ui/aria/aria.js'; +import * as dom from '../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js'; -import { KeyCode } from '../../../base/common/keyCodes.js'; -import { OS } from '../../../base/common/platform.js'; -import { memoize } from '../../../base/common/decorators.js'; +import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; +import { AriaRole } from '../../../base/browser/ui/aria/aria.js'; +import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; +import { IHoverDelegate } from '../../../base/browser/ui/hover/hoverDelegate.js'; +import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; import { IIconLabelValueOptions, IconLabel } from '../../../base/browser/ui/iconLabel/iconLabel.js'; import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; -import { isDark } from '../../theme/common/theme.js'; -import { URI } from '../../../base/common/uri.js'; -import { quickInputButtonToAction } from './quickInputUtils.js'; -import { Lazy } from '../../../base/common/lazy.js'; -import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from '../../../base/common/iconLabels.js'; -import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; -import { compareAnything } from '../../../base/common/comparers.js'; -import { escape, ltrim } from '../../../base/common/strings.js'; +import { IListVirtualDelegate } from '../../../base/browser/ui/list/list.js'; +import { IListAccessibilityProvider, IListStyles } from '../../../base/browser/ui/list/listWidget.js'; +import { Checkbox } from '../../../base/browser/ui/toggle/toggle.js'; import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; -import { ThrottledDelayer } from '../../../base/common/async.js'; -import { isCancellationError } from '../../../base/common/errors.js'; -import type { IHoverWidget, IManagedHoverTooltipMarkdownString } from '../../../base/browser/ui/hover/hover.js'; -import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; -import { observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; import { equals } from '../../../base/common/arrays.js'; +import { ThrottledDelayer } from '../../../base/common/async.js'; +import { compareAnything } from '../../../base/common/comparers.js'; +import { memoize } from '../../../base/common/decorators.js'; +import { isCancellationError } from '../../../base/common/errors.js'; +import { Emitter, Event, EventBufferer, IValueWithChangeEvent } from '../../../base/common/event.js'; +import { IMatch } from '../../../base/common/filters.js'; +import { IMarkdownString } from '../../../base/common/htmlContent.js'; +import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from '../../../base/common/iconLabels.js'; +import { KeyCode } from '../../../base/common/keyCodes.js'; +import { Lazy } from '../../../base/common/lazy.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; +import { observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; +import { OS } from '../../../base/common/platform.js'; +import { escape, ltrim } from '../../../base/common/strings.js'; +import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; +import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { WorkbenchObjectTree } from '../../list/browser/listService.js'; +import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; +import { isDark } from '../../theme/common/theme.js'; +import { IThemeService } from '../../theme/common/themeService.js'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickFocus, QuickPickItem } from '../common/quickInput.js'; +import { quickInputButtonToAction } from './quickInputUtils.js'; const $ = dom.$; @@ -67,8 +69,9 @@ interface IQuickPickElement extends IQuickInputItemLazyParts { interface IQuickInputItemTemplateData { entry: HTMLDivElement; - checkbox: HTMLInputElement; + checkbox: MutableDisposable; icon: HTMLDivElement; + outerLabel: HTMLElement; label: IconLabel; keybinding: KeybindingLabel; detail: IconLabel; @@ -320,7 +323,7 @@ abstract class BaseQuickInputListRenderer implement abstract templateId: string; constructor( - private readonly hoverDelegate: IHoverDelegate | undefined + private readonly hoverDelegate: IHoverDelegate | undefined, ) { } // TODO: only do the common stuff here and have a subclass handle their specific stuff @@ -332,13 +335,16 @@ abstract class BaseQuickInputListRenderer implement // Checkbox const label = dom.append(data.entry, $('label.quick-input-list-label')); + data.outerLabel = label; + data.checkbox = data.toDisposeTemplate.add(new MutableDisposable()); data.toDisposeTemplate.add(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { - if (!data.checkbox.offsetParent) { // If checkbox not visible: - e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740 + // `label` elements with role=checkboxes don't automatically toggle them like normal elements + if (data.checkbox.value && !e.defaultPrevented && data.checkbox.value.enabled) { + const checked = !data.checkbox.value.checked; + data.checkbox.value.checked = checked; + (data.element as QuickPickItemElement).checked = checked; } })); - data.checkbox = dom.append(label, $('input.quick-input-list-checkbox')); - data.checkbox.type = 'checkbox'; // Rows const rows = dom.append(label, $('.quick-input-list-rows')); @@ -402,14 +408,31 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer { - (data.element as QuickPickItemElement).checked = data.checkbox.checked; - })); + let checkbox = data.checkbox.value; + if (!checkbox) { + checkbox = new Checkbox(element.saneLabel, element.checked, { ...defaultCheckboxStyles, size: 15 }); + data.checkbox.value = checkbox; + data.outerLabel.prepend(checkbox.domNode); + } else { + checkbox.setTitle(element.saneLabel); + } - return data; + if (element.checkboxDisabled) { + checkbox.disable(); + } else { + checkbox.enable(); + } + + checkbox.checked = element.checked; + data.toDisposeElement.add(element.onChecked(checked => checkbox.checked = checked)); + data.toDisposeElement.add(checkbox.onChange(() => element.checked = checkbox.checked)); } renderElement(node: ITreeNode, index: number, data: IQuickInputItemTemplateData): void { @@ -421,9 +444,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer data.checkbox.checked = checked)); - data.checkbox.disabled = element.checkboxDisabled; + this.ensureCheckbox(element, data); const { labelHighlights, descriptionHighlights, detailHighlights } = element; @@ -557,12 +578,6 @@ class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer, index: number, data: IQuickInputItemTemplateData): void { const element = node.element; data.element = element; @@ -1089,7 +1104,7 @@ export class QuickInputTree extends Disposable { } const qpi = new QuickPickItemElement( index, - this._hasCheckboxes, + this._hasCheckboxes && item.pickable !== false, e => this._onButtonTriggered.fire(e), this._elementChecked, item, diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 696b3b77c75..e0ba3516012 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -354,7 +354,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe handleCommandFinished(exitCode: number | undefined, options?: IHandleCommandOptions): void { // Command executed may not have happened yet, if not handle it now so the expected events - // properly propogate. This may cause the output to show up in the computed command line, + // properly propagate. This may cause the output to show up in the computed command line, // but the command line confidence will be low in the extension host for example and // therefore cannot be trusted anyway. if (!this._currentCommand.commandExecutedMarker) { diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 9065497b3fa..a380ed4089e 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -1009,6 +1009,17 @@ export const enum ShellIntegrationInjectionFailureReason { * won't have shell integration in the end. */ UnsupportedShell = 'unsupportedShell', + + + /** + * For zsh, we failed to set the sticky bit on the shell integration script folder. + */ + FailedToSetStickyBit = 'failedToSetStickyBit', + + /** + * For zsh, we failed to create a temp directory for the shell integration script. + */ + FailedToCreateTmpDir = 'failedToCreateTmpDir', } export enum TerminalExitReason { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 502e45f8c85..2acd35de838 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -15,7 +15,7 @@ import { IShellLaunchConfig, ITerminalEnvironment, ITerminalProcessOptions, Shel import { EnvironmentVariableMutatorType } from '../common/environmentVariable.js'; import { deserializeEnvironmentVariableCollections } from '../common/environmentVariableShared.js'; import { MergedEnvironmentVariableCollection } from '../common/environmentVariableCollection.js'; -import { chmod, realpathSync } from 'fs'; +import { chmod, realpathSync, mkdirSync } from 'fs'; import { promisify } from 'util'; export function getWindowsBuildNumber(): number { @@ -98,14 +98,16 @@ export async function getShellIntegrationInjection( if (options.shellIntegration.nonce) { envMixin['VSCODE_NONCE'] = options.shellIntegration.nonce; } + // Temporarily pass list of hardcoded env vars for shell env api + const scopedDownShellEnvs = ['PATH', 'VIRTUAL_ENV', 'HOME', 'SHELL', 'PWD']; if (shellLaunchConfig.shellIntegrationEnvironmentReporting) { if (isWindows) { const enableWindowsEnvReporting = options.windowsUseConptyDll || options.windowsEnableConpty && getWindowsBuildNumber() >= 22631 && shell !== 'bash.exe'; if (enableWindowsEnvReporting) { - envMixin['VSCODE_SHELL_ENV_REPORTING'] = '1'; + envMixin['VSCODE_SHELL_ENV_REPORTING'] = scopedDownShellEnvs.join(','); } } else { - envMixin['VSCODE_SHELL_ENV_REPORTING'] = '1'; + envMixin['VSCODE_SHELL_ENV_REPORTING'] = scopedDownShellEnvs.join(','); } } // Windows @@ -240,8 +242,23 @@ export async function getShellIntegrationInjection( const chmodAsync = promisify(chmod); await chmodAsync(zdotdir, 0o1700); } catch (err) { + if (err.message.includes('ENOENT')) { + try { + mkdirSync(zdotdir); + } catch (err) { + logService.error(`Failed to create zdotdir at ${zdotdir}: ${err}`); + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToCreateTmpDir }; + } + try { + const chmodAsync = promisify(chmod); + await chmodAsync(zdotdir, 0o1700); + } catch { + logService.error(`Failed to set sticky bit on ${zdotdir}: ${err}`); + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToSetStickyBit }; + } + } logService.error(`Failed to set sticky bit on ${zdotdir}: ${err}`); - return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedShell }; + return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.FailedToSetStickyBit }; } } envMixin['ZDOTDIR'] = zdotdir; diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index 33ee6b363cd..1d61d3eec44 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from '../../../base/browser/ui/button/button.js'; import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder } from '../common/colorRegistry.js'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssVariable, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow, badgeBackground, badgeForeground, breadcrumbsBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground, activeContrastBorder, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropOverBackground, listFocusAndSelectionOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, tableColumnsBorder, tableOddRowsBackgroundColor, treeIndentGuidesStroke, asCssVariableWithDefault, editorWidgetBorder, focusBorder, pickerGroupForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, selectBackground, selectBorder, selectForeground, selectListBackground, treeInactiveIndentGuidesStroke, menuBorder, menuForeground, menuBackground, menuSelectionForeground, menuSelectionBackground, menuSelectionBorder, menuSeparatorBackground, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listDropBetweenBackground, radioActiveBackground, radioActiveForeground, radioInactiveBackground, radioInactiveForeground, radioInactiveBorder, radioInactiveHoverBackground, radioActiveBorder, checkboxDisabledBackground, checkboxDisabledForeground } from '../common/colorRegistry.js'; import { IProgressBarStyles } from '../../../base/browser/ui/progressbar/progressbar.js'; import { ICheckboxStyles, IToggleStyles } from '../../../base/browser/ui/toggle/toggle.js'; import { IDialogStyles } from '../../../base/browser/ui/dialog/dialog.js'; @@ -89,13 +89,11 @@ export function getToggleStyles(override: IStyleOverride): IToggl export const defaultCheckboxStyles: ICheckboxStyles = { checkboxBackground: asCssVariable(checkboxBackground), checkboxBorder: asCssVariable(checkboxBorder), - checkboxForeground: asCssVariable(checkboxForeground) + checkboxForeground: asCssVariable(checkboxForeground), + checkboxDisabledBackground: asCssVariable(checkboxDisabledBackground), + checkboxDisabledForeground: asCssVariable(checkboxDisabledForeground), }; -export function getCheckboxStyles(override: IStyleOverride): ICheckboxStyles { - return overrideStyles(override, defaultCheckboxStyles); -} - export const defaultDialogStyles: IDialogStyles = { dialogBackground: asCssVariable(editorWidgetBackground), dialogForeground: asCssVariable(editorWidgetForeground), diff --git a/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts index 74a83383b05..f55c8aad640 100644 --- a/src/vs/platform/theme/common/colorUtils.ts +++ b/src/vs/platform/theme/common/colorUtils.ts @@ -51,7 +51,8 @@ export const enum ColorTransformType { Opaque, OneOf, LessProminent, - IfDefinedThenElse + IfDefinedThenElse, + Mix, } export type ColorTransform = @@ -61,7 +62,8 @@ export type ColorTransform = | { op: ColorTransformType.Opaque; value: ColorValue; background: ColorValue } | { op: ColorTransformType.OneOf; values: readonly ColorValue[] } | { op: ColorTransformType.LessProminent; value: ColorValue; background: ColorValue; factor: number; transparency: number } - | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue }; + | { op: ColorTransformType.IfDefinedThenElse; if: ColorIdentifier; then: ColorValue; else: ColorValue } + | { op: ColorTransformType.Mix; color: ColorValue; with: ColorValue; ratio?: number }; export interface ColorDefaults { light: ColorValue | null; @@ -256,6 +258,12 @@ export function executeTransform(transform: ColorTransform, theme: IColorTheme): case ColorTransformType.Transparent: return resolveColorValue(transform.value, theme)?.transparent(transform.factor); + case ColorTransformType.Mix: { + const primaryColor = resolveColorValue(transform.color, theme) || Color.transparent; + const otherColor = resolveColorValue(transform.with, theme) || Color.transparent; + return primaryColor.mix(otherColor, transform.ratio); + } + case ColorTransformType.Opaque: { const backgroundColor = resolveColorValue(transform.background, theme); if (!backgroundColor) { diff --git a/src/vs/platform/theme/common/colors/inputColors.ts b/src/vs/platform/theme/common/colors/inputColors.ts index 1cf5a83e85a..f6a1348a6ae 100644 --- a/src/vs/platform/theme/common/colors/inputColors.ts +++ b/src/vs/platform/theme/common/colors/inputColors.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; // Import the effects we need import { Color, RGBA } from '../../../../base/common/color.js'; -import { registerColor, transparent, lighten, darken } from '../colorUtils.js'; +import { registerColor, transparent, lighten, darken, ColorTransformType } from '../colorUtils.js'; // Import the colors we need import { foreground, contrastBorder, focusBorder, iconForeground } from './baseColors.js'; @@ -193,6 +193,14 @@ export const checkboxSelectBorder = registerColor('checkbox.selectBorder', iconForeground, nls.localize('checkbox.select.border', "Border color of checkbox widget when the element it's in is selected.")); +export const checkboxDisabledBackground = registerColor('checkbox.disabled.background', + { op: ColorTransformType.Mix, color: checkboxBackground, with: checkboxForeground, ratio: 0.33 }, + nls.localize('checkbox.disabled.background', "Background of a disabled checkbox.")); + +export const checkboxDisabledForeground = registerColor('checkbox.disabled.foreground', + { op: ColorTransformType.Mix, color: checkboxForeground, with: checkboxBackground, ratio: 0.33 }, + nls.localize('checkbox.disabled.foreground', "Foreground of a disabled checkbox.")); + // ------ keybinding label diff --git a/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts b/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts index 8c432401777..734c07abf07 100644 --- a/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts +++ b/src/vs/platform/userDataSync/common/promptsSync/promptsSync.ts @@ -7,13 +7,13 @@ import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { isPromptFile } from '../../../prompts/common/constants.js'; import { IStorageService } from '../../../storage/common/storage.js'; import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js'; import { IEnvironmentService } from '../../../environment/common/environment.js'; +import { isPromptOrInstructionsFile } from '../../../prompts/common/constants.js'; import { IUserDataProfile } from '../../../userDataProfile/common/userDataProfile.js'; import { IConfigurationService } from '../../../configuration/common/configuration.js'; import { areSame, IMergeResult as IPromptsMergeResult, merge } from './promptsMerge.js'; @@ -517,7 +517,7 @@ export class PromptsSynchronizer extends AbstractSynchroniser implements IUserDa for (const entry of stat.children || []) { const resource = entry.resource; - if (!isPromptFile(resource)) { + if (isPromptOrInstructionsFile(resource) === false) { continue; } diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index 547e3346694..152c23193e2 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -37,6 +37,7 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo requestId, codeBlocks: uiRequest.codeBlocks, chatRequestId: uiRequest.chatRequestId, + chatRequestModel: uiRequest.chatRequestModel, location: uiRequest.location }; try { diff --git a/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts b/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts index db70e241abc..ae1198c5455 100644 --- a/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadEditSessionIdentityParticipant.ts @@ -16,7 +16,7 @@ import { WorkspaceFolder } from '../../../platform/workspace/common/workspace.js class ExtHostEditSessionIdentityCreateParticipant implements IEditSessionIdentityCreateParticipant { private readonly _proxy: ExtHostWorkspaceShape; - private readonly timeout = 10000; + private readonly timeout = 20000; constructor(extHostContext: IExtHostContext) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index a61ba3c7801..1a7074347d4 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -397,10 +397,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } try { - const scmQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.isSCM); - const scmQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.label === scmQuickDiff?.label); + const primaryQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const primaryQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.label === primaryQuickDiff?.label); - return Promise.resolve(scmQuickDiffChanges.map(change => change.change) ?? []); + return Promise.resolve(primaryQuickDiffChanges.map(change => change.change) ?? []); } finally { quickDiffModelRef.dispose(); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index cd7c6d9f883..bff5ae2f2a0 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -6,6 +6,7 @@ import { AsyncIterableSource, DeferredPromise } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; +import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { SerializedError, transformErrorForSerialization, transformErrorFromSerialization } from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; @@ -153,7 +154,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { } this._logService.trace('[CHAT] request DONE', extension.value, requestId); } catch (err) { - this._logService.error('[CHAT] extension request ERRORED in STREAM', err, extension.value, requestId); + this._logService.error('[CHAT] extension request ERRORED in STREAM', toErrorMessage(err, true), extension.value, requestId); this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); } })(); @@ -163,7 +164,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._logService.debug('[CHAT] extension request DONE', extension.value, requestId); this._proxy.$acceptResponseDone(requestId, undefined); }, err => { - this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId); + this._logService.error('[CHAT] extension request ERRORED', toErrorMessage(err, true), extension.value, requestId); this._proxy.$acceptResponseDone(requestId, transformErrorForSerialization(err)); }); } diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 097a4a4d16d..c93065c1b53 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -9,11 +9,12 @@ import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; -import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; +import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; +import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadMcp) export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @@ -21,6 +22,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { private _serverIdCounter = 0; private readonly _servers = new Map(); + private readonly _proxy: Proxied; private readonly _collectionDefinitions = this._register(new DisposableMap; @@ -32,7 +34,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, ) { super(); - const proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); + const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); this._register(this._mcpRegistry.registerDelegate({ // Prefer Node.js extension hosts when they're available. No CORS issues etc. priority: _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1, @@ -40,7 +42,6 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { return proxy.$waitForInitialCollectionProviders(); }, canStart(collection, serverDefinition) { - // todo: SSE MPC servers without a remote authority could be served from the renderer if (collection.remoteAuthority !== _extHostContext.remoteAuthority) { return false; } @@ -74,6 +75,10 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { const serverDefinitions = observableValue('mcpServers', servers); const handle = this._mcpRegistry.registerCollection({ ...collection, + resolveServerLanch: collection.canResolveLaunch ? (async def => { + const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); + return r ? McpServerLaunch.fromSerialized(r) : undefined; + }) : undefined, remoteAuthority: this._extHostContext.remoteAuthority, serverDefinitions, }); @@ -148,7 +153,11 @@ class ExtHostMcpServerLaunch extends Disposable implements IMcpMessageTransport } if (parsed) { - this._onDidReceiveMessage.fire(parsed); + if (Array.isArray(parsed)) { // streamable HTTP supports batching + parsed.forEach(p => this._onDidReceiveMessage.fire(p)); + } else { + this._onDidReceiveMessage.fire(parsed); + } } } diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index d86233aabf9..1db282c0abe 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -28,8 +28,8 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { label, rootUri: URI.revive(rootUri), selector, - isSCM: false, visible, + kind: 'contributed', getOriginalResource: async (uri: URI) => { return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 2e33d6f89a2..86387c27651 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -16,7 +16,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; +import { IQuickDiffService } from '../../contrib/scm/common/quickDiff.js'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; @@ -235,7 +235,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { } } -class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { +class MainThreadSCMProvider implements ISCMProvider { private static ID_HANDLE = 0; private _id = `scm${MainThreadSCMProvider.ID_HANDLE++}`; @@ -287,8 +287,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get actionButton(): IObservable { return this._actionButton; } private _quickDiff: IDisposable | undefined; - public readonly isSCM: boolean = true; - public readonly visible: boolean = true; + private _stagedQuickDiff: IDisposable | undefined; private readonly _historyProvider = observableValue(this, undefined); get historyProvider() { return this._historyProvider; } @@ -337,15 +336,42 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._quickDiff = this._quickDiffService.addQuickDiffProvider({ label: features.quickDiffLabel ?? this.label, rootUri: this.rootUri, - isSCM: this.isSCM, - visible: this.visible, - getOriginalResource: (uri: URI) => this.getOriginalResource(uri) + visible: true, + kind: 'primary', + getOriginalResource: async (uri: URI) => { + if (!this.features.hasQuickDiffProvider) { + return null; + } + + const result = await this.proxy.$provideOriginalResource(this.handle, uri, CancellationToken.None); + return result && URI.revive(result); + } }); } else if (features.hasQuickDiffProvider === false && this._quickDiff) { this._quickDiff.dispose(); this._quickDiff = undefined; } + if (features.hasSecondaryQuickDiffProvider && !this._stagedQuickDiff) { + this._stagedQuickDiff = this._quickDiffService.addQuickDiffProvider({ + label: features.secondaryQuickDiffLabel ?? this.label, + rootUri: this.rootUri, + visible: true, + kind: 'secondary', + getOriginalResource: async (uri: URI) => { + if (!this.features.hasSecondaryQuickDiffProvider) { + return null; + } + + const result = await this.proxy.$provideSecondaryOriginalResource(this.handle, uri, CancellationToken.None); + return result && URI.revive(result); + } + }); + } else if (features.hasSecondaryQuickDiffProvider === false && this._stagedQuickDiff) { + this._stagedQuickDiff.dispose(); + this._stagedQuickDiff = undefined; + } + if (features.hasHistoryProvider && !this.historyProvider.get()) { const historyProvider = new MainThreadSCMHistoryProvider(this.proxy, this.handle); this._historyProvider.set(historyProvider, undefined); diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 9f7772a2ffd..e8a72e6c59a 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -14,6 +14,7 @@ import { ExtHostContext, ExtHostSearchShape, MainContext, MainThreadSearchShape import { revive } from '../../../base/common/marshalling.js'; import * as Constants from '../../contrib/search/common/constants.js'; import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; +import { AISearchKeyword } from '../../services/search/common/searchExtTypes.js'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -72,6 +73,16 @@ export class MainThreadSearch implements MainThreadSearchShape { provider.handleFindMatch(session, data); } + + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void { + const provider = this._searchProvider.get(handle); + if (!provider) { + throw new Error('Got result for unknown provider'); + } + + provider.handleKeywordResult(session, data); + } + $handleTelemetry(eventName: string, data: any): void { this._telemetryService.publicLog(eventName, data); } @@ -84,7 +95,8 @@ class SearchOperation { constructor( readonly progress?: (match: IFileMatch) => any, readonly id: number = ++SearchOperation._idPool, - readonly matches = new Map() + readonly matches = new Map(), + readonly keywords: AISearchKeyword[] = [] ) { // } @@ -104,6 +116,10 @@ class SearchOperation { this.progress?.(match); } + + addKeyword(result: AISearchKeyword): void { + this.keywords.push(result); + } } class RemoteSearchProvider implements ISearchResultProvider, IDisposable { @@ -153,7 +169,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { return Promise.resolve(searchP).then((result: ISearchCompleteStats) => { this._searches.delete(search.id); - return { results: Array.from(search.matches.values()), stats: result.stats, limitHit: result.limitHit, messages: result.messages }; + return { results: Array.from(search.matches.values()), aiKeywords: Array.from(search.keywords), stats: result.stats, limitHit: result.limitHit, messages: result.messages }; }, err => { this._searches.delete(search.id); return Promise.reject(err); @@ -183,7 +199,17 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { }); } - private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken): Promise { + handleKeywordResult(session: number, data: AISearchKeyword): void { + const searchOp = this._searches.get(session); + + if (!searchOp) { + // ignore... + return; + } + searchOp.addKeyword(data); + } + + private _provideSearchResults(query: ISearchQuery, session: number, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { switch (query.type) { case QueryType.File: return this._proxy.$provideFileSearchResults(this._handle, session, query, token); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d6b148a7414..403e940caa1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -27,7 +27,7 @@ import { ExtensionDescriptionRegistry } from '../../services/extensions/common/e import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; +import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2, AISearchKeyword } from '../../services/search/common/searchExtTypes.js'; import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from './extHost.protocol.js'; import { ExtHostRelatedInformation } from './extHostAiRelatedInformation.js'; import { ExtHostApiCommands } from './extHostApiCommands.js'; @@ -1238,10 +1238,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'canonicalUriProvider'); return extHostWorkspace.provideCanonicalUri(uri, options, token); }, - decode(content: Uint8Array, options: { uri?: vscode.Uri; encoding?: string }) { + decode(content: Uint8Array, options?: { uri?: vscode.Uri; encoding?: string }) { return extHostWorkspace.decode(content, options); }, - encode(content: string, options: { uri?: vscode.Uri; encoding?: string }) { + encode(content: string, options?: { uri?: vscode.Uri; encoding?: string }) { return extHostWorkspace.encode(content, options); } }; @@ -1515,7 +1515,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get tools() { return extHostLanguageModelTools.getTools(extension); }, - fileIsIgnored(uri: vscode.Uri, token: vscode.CancellationToken) { + fileIsIgnored(uri: vscode.Uri, token?: vscode.CancellationToken) { return extHostLanguageModels.fileIsIgnored(extension, uri, token); }, registerIgnoredFileProvider(provider: vscode.LanguageModelIgnoredFileProvider) { @@ -1797,13 +1797,16 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatResponseMovePart: extHostTypes.ChatResponseMovePart, + ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, + ChatRequestTurn2: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, ChatRequestEditorData: extHostTypes.ChatRequestEditorData, ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, ChatReferenceBinaryData: extHostTypes.ChatReferenceBinaryData, + ChatRequestEditedFileEventKind: extHostTypes.ChatRequestEditedFileEventKind, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, @@ -1815,6 +1818,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageModelToolResult: extHostTypes.LanguageModelToolResult, LanguageModelToolResult2: extHostTypes.LanguageModelToolResult2, LanguageModelDataPart: extHostTypes.LanguageModelDataPart, + LanguageModelExtraDataPart: extHostTypes.LanguageModelExtraDataPart, ChatImageMimeType: extHostTypes.ChatImageMimeType, ExtendedLanguageModelToolResult: extHostTypes.ExtendedLanguageModelToolResult, PreparedTerminalToolInvocation: extHostTypes.PreparedTerminalToolInvocation, @@ -1828,9 +1832,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExcludeSettingOptions: ExcludeSettingOptions, TextSearchContext2: TextSearchContext2, TextSearchMatch2: TextSearchMatch2, + AISearchKeyword: AISearchKeyword, TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, ChatErrorLevel: extHostTypes.ChatErrorLevel, - McpSSEServerDefinition: extHostTypes.McpSSEServerDefinition, + McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, }; }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b0bd01e9f68..28953da827b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -85,7 +85,7 @@ import { OutputChannelUpdateMode } from '../../services/output/common/output.js' import { CandidatePort } from '../../services/remote/common/tunnelModel.js'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js'; import * as search from '../../services/search/common/search.js'; -import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; +import { AISearchKeyword, TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -1524,6 +1524,7 @@ export interface MainThreadSearchShape extends IDisposable { $unregisterProvider(handle: number): void; $handleFileMatch(handle: number, session: number, data: UriComponents[]): void; $handleTextMatch(handle: number, session: number, data: search.IRawFileMatch2[]): void; + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void; $handleTelemetry(eventName: string, data: any): void; } @@ -1560,6 +1561,8 @@ export interface SCMProviderFeatures { hasHistoryProvider?: boolean; hasQuickDiffProvider?: boolean; quickDiffLabel?: string; + hasSecondaryQuickDiffProvider?: boolean; + secondaryQuickDiffLabel?: string; count?: number; commitTemplate?: string; acceptInputCommand?: languages.Command; @@ -2532,6 +2535,7 @@ export interface ExtHostTerminalShellIntegrationShape { export interface ExtHostSCMShape { $provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; + $provideSecondaryOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise; $onInputBoxValueChange(sourceControlHandle: number, value: string): void; $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise; $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; @@ -2973,6 +2977,7 @@ export interface ExtHostTestingShape { } export interface ExtHostMcpShape { + $resolveMcpLaunch(collectionId: string, label: string): Promise; $startMcp(id: number, launch: McpServerLaunch.Serialized): void; $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; @@ -2983,7 +2988,7 @@ export interface MainThreadMcpShape { $onDidChangeState(id: number, state: McpConnectionState): void; $onDidPublishLog(id: number, level: LogLevel, log: string): void; $onDidReceiveMessage(id: number, message: string): void; - $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: Dto[]): void; + $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void; $deleteMcpCollection(collectionId: string): void; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 4afd5db26ee..49c7a5feff3 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -255,6 +255,7 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseConfirmationPart || part instanceof extHostTypes.ChatResponseCodeCitationPart || part instanceof extHostTypes.ChatResponseMovePart || + part instanceof extHostTypes.ChatResponseExtensionsPart || part instanceof extHostTypes.ChatResponseProgressPart2 ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -409,8 +410,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const model = await this.getModelForRequest(request, detector.extension); - const includeInteractionId = isProposedApiEnabled(detector.extension, 'chatParticipantPrivate'); - const extRequest = typeConvert.ChatAgentRequest.to(includeInteractionId ? request : { ...request, requestId: '' }, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request)); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request), detector.extension); return detector.provider.provideParticipantDetection( extRequest, @@ -494,13 +494,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); const model = await this.getModelForRequest(request, agent.extension); - const includeInteractionId = isProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); const extRequest = typeConvert.ChatAgentRequest.to( - includeInteractionId ? request : { ...request, requestId: '' }, + request, location, model, this.getDiagnosticsWhenEnabled(agent.extension), - this.getToolsForRequest(agent.extension, request) + this.getToolsForRequest(agent.extension, request), + agent.extension ); inFlightRequest = { requestId: requestDto.requestId, extRequest }; this._inFlightRequests.add(inFlightRequest); @@ -584,7 +584,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const toolReferences = h.request.variables.variables .filter(v => v.kind === 'tool') .map(typeConvert.ChatLanguageModelToolReference.to); - const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences); + const editedFileEvents = isProposedApiEnabled(extension, 'chatParticipantPrivate') ? h.request.editedFileEvents : undefined; + const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents); res.push(turn); // RESPONSE turn diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index 5f573f995d9..1039d66155a 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -52,6 +52,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape const request: vscode.MappedEditsRequest = { location: internalRequest.location, chatRequestId: internalRequest.chatRequestId, + chatRequestModel: internalRequest.chatRequestModel, codeBlocks: internalRequest.codeBlocks.map(block => { return { code: block.code, diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index d047fc69cdf..a641a5c81f6 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,13 +12,14 @@ import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/editFileTool.js'; +import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/tools.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; -import * as typeConvert from './extHostTypeConverters.js'; -import { InternalFetchWebPageToolId, IToolInputProcessor } from '../../contrib/chat/common/tools/tools.js'; -import { EditToolData, InternalEditToolId, EditToolInputProcessor, ExtensionEditToolId } from '../../contrib/chat/common/tools/editFileTool.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; +import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import { ExtHostLanguageModels } from './extHostLanguageModels.js'; +import * as typeConvert from './extHostTypeConverters.js'; +import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { /** A map of tools that were registered in this EH */ @@ -29,8 +30,6 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape /** A map of all known tools, from other EHs or registered in vscode core */ private readonly _allTools = new Map(); - private readonly _toolInputProcessors = new Map(); - constructor( mainContext: IMainContext, private readonly _languageModels: ExtHostLanguageModels, @@ -42,8 +41,6 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape this._allTools.set(tool.id, revive(tool)); } }); - - this._toolInputProcessors.set(EditToolData.id, new EditToolInputProcessor()); } async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { @@ -71,11 +68,10 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape } // Making the round trip here because not all tools were necessarily registered in this EH - const processedInput = this._toolInputProcessors.get(toolId)?.processInput(options.input) ?? options.input; const result = await this._proxy.$invokeTool({ toolId, callId, - parameters: processedInput, + parameters: options.input, tokenBudget: options.tokenizationOptions?.tokenBudget, context: options.toolInvocationToken as IToolInvocationContext | undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, @@ -102,6 +98,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape case InternalEditToolId: case ExtensionEditToolId: case InternalFetchWebPageToolId: + case SearchExtensionsToolId: return isProposedApiEnabled(extension, 'chatParticipantPrivate'); default: return true; diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index cef86211e81..3885d97ca22 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -176,6 +176,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), name: metadata.name ?? '', family: metadata.family ?? '', + description: metadata.description, version: metadata.version, maxInputTokens: metadata.maxInputTokens, maxOutputTokens: metadata.maxOutputTokens, @@ -560,7 +561,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { }; } - fileIsIgnored(extension: IExtensionDescription, uri: vscode.Uri, token: vscode.CancellationToken): Promise { + fileIsIgnored(extension: IExtensionDescription, uri: vscode.Uri, token: vscode.CancellationToken = CancellationToken.None): Promise { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); return this._proxy.$fileIsIgnored(uri, token); diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 1cc13119737..e8ccfc48f4d 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -3,20 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as ES from '@c4312/eventsource-umd'; import * as vscode from 'vscode'; -import { importAMDNodeModule } from '../../../amdX.js'; -import { DeferredPromise, Sequencer } from '../../../base/common/async.js'; -import { CancellationToken } from '../../../base/common/cancellation.js'; -import { Lazy } from '../../../base/common/lazy.js'; +import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { SSEParser } from '../../../base/common/sseParser.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; +import { LogLevel } from '../../../platform/log/common/log.js'; import { StorageScope } from '../../../platform/storage/common/storage.js'; -import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportSSE, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; import { IExtHostRpcService } from './extHostRpcService.js'; -import { LogLevel } from '../../../platform/log/common/log.js'; +import * as Convert from './extHostTypeConverters.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); @@ -27,11 +26,11 @@ export interface IExtHostMpcService extends ExtHostMcpShape { export class ExtHostMcpService extends Disposable implements IExtHostMpcService { protected _proxy: MainThreadMcpShape; private readonly _initialProviderPromises = new Set>(); - private readonly _sseEventSources = this._register(new DisposableMap()); - private readonly _eventSource = new Lazy(async () => { - const es = await importAMDNodeModule('@c4312/eventsource-umd', 'dist/index.umd.js'); - return es.EventSource; - }); + private readonly _sseEventSources = this._register(new DisposableMap()); + private readonly _unresolvedMcpServers = new Map(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -45,8 +44,8 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService } protected _startMcp(id: number, launch: McpServerLaunch): void { - if (launch.type === McpServerTransportType.SSE) { - this._sseEventSources.set(id, new McpSSEHandle(this._eventSource.value, id, launch, this._proxy)); + if (launch.type === McpServerTransportType.HTTP) { + this._sseEventSources.set(id, new McpHTTPHandle(id, launch, this._proxy)); return; } @@ -68,6 +67,24 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService await Promise.all(this._initialProviderPromises); } + async $resolveMcpLaunch(collectionId: string, label: string): Promise { + const rec = this._unresolvedMcpServers.get(collectionId); + if (!rec) { + return; + } + + const server = rec.servers.find(s => s.label === label); + if (!server) { + return; + } + if (!rec.provider.resolveMcpServerDefinition) { + return Convert.McpServerDefinition.from(server); + } + + const resolved = await rec.provider.resolveMcpServerDefinition(server, CancellationToken.None); + return resolved ? Convert.McpServerDefinition.from(resolved) : undefined; + } + /** {@link vscode.lm.registerMcpConfigurationProvider} */ public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { const store = new DisposableStore(); @@ -81,37 +98,21 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService id: extensionPrefixedIdentifier(extension.identifier, id), isTrustedByDefault: true, label: metadata?.label ?? extension.displayName ?? extension.name, - scope: StorageScope.WORKSPACE + scope: StorageScope.WORKSPACE, + canResolveLaunch: typeof provider.resolveMcpServerDefinition === 'function', + extensionId: extension.identifier.value, }; const update = async () => { - const list = await provider.provideMcpServerDefinitions(CancellationToken.None); + this._unresolvedMcpServers.set(mcp.id, { servers: list ?? [], provider }); - function isSSEConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpSSEServerDefinition { - return !!(candidate as vscode.McpSSEServerDefinition).uri; - } - - const servers: McpServerDefinition[] = []; - + const servers: McpServerDefinition.Serialized[] = []; for (const item of list ?? []) { servers.push({ id: ExtensionIdentifier.toKey(extension.identifier), label: item.label, - launch: isSSEConfig(item) - ? { - type: McpServerTransportType.SSE, - uri: item.uri, - headers: item.headers, - } - : { - type: McpServerTransportType.Stdio, - cwd: item.cwd, - args: item.args, - command: item.command, - env: item.env, - envFile: undefined, - } + launch: Convert.McpServerDefinition.from(item) }); } @@ -119,6 +120,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService }; store.add(toDisposable(() => { + this._unresolvedMcpServers.delete(mcp.id); this._proxy.$deleteMcpCollection(mcp.id); })); @@ -139,97 +141,304 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService } } -class McpSSEHandle extends Disposable { +const enum HttpMode { + Unknown, + Http, + SSE, +} + +type HttpModeT = + | { value: HttpMode.Unknown } + | { value: HttpMode.Http; sessionId: string | undefined } + | { value: HttpMode.SSE; endpoint: string }; + +/** + * Implementation of both MCP HTTP Streaming as well as legacy SSE. + * + * The first request will POST to the endpoint, assuming HTTP streaming. If the + * server is legacy SSE, it should return some 4xx status in that case, + * and we'll automatically fall back to SSE and res + */ +class McpHTTPHandle extends Disposable { private readonly _requestSequencer = new Sequencer(); - private readonly _postEndpoint = new DeferredPromise<{ url: string; transport: McpServerTransportSSE }>(); + private readonly _postEndpoint = new DeferredPromise<{ url: string; transport: McpServerTransportHTTP }>(); + private _mode: HttpModeT = { value: HttpMode.Unknown }; + private readonly _cts = new CancellationTokenSource(); + private readonly _abortCtrl = new AbortController(); + constructor( - eventSourceCtor: Promise, private readonly _id: number, - launch: McpServerTransportSSE, + private readonly _launch: McpServerTransportHTTP, private readonly _proxy: MainThreadMcpShape ) { super(); - eventSourceCtor.then(EventSourceCtor => this._attach(EventSourceCtor, launch)); - } - private _attach(EventSourceCtor: typeof ES.EventSource, launch: McpServerTransportSSE) { - if (this._store.isDisposed) { - return; - } - - const eventSource = new EventSourceCtor(launch.uri.toString(), { - // recommended way to do things https://github.com/EventSource/eventsource?tab=readme-ov-file#setting-http-request-headers - fetch: (input, init) => - fetch(input, { - ...init, - headers: { - ...Object.fromEntries(launch.headers), - ...init?.headers, - }, - }).then(async res => { - // we get more details on failure at this point, so handle it explicitly: - if (res.status >= 300) { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${launch.uri}: ${await this._getErrText(res)}` }); - eventSource.close(); - } - return res; - }, err => { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${launch.uri}: ${String(err)}` }); - - eventSource.close(); - return Promise.reject(err); - }) - }); - - this._register(toDisposable(() => eventSource.close())); - - // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L52 - eventSource.addEventListener('endpoint', e => { - this._postEndpoint.complete({ transport: launch, url: new URL(e.data, launch.uri.toString()).toString() }); - }); - - // https://github.com/modelcontextprotocol/typescript-sdk/blob/0fa2397174eba309b54575294d56754c52b13a65/src/server/sse.ts#L133 - eventSource.addEventListener('message', e => { - this._proxy.$onDidReceiveMessage(this._id, e.data); - }); - - eventSource.addEventListener('open', () => { - this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running }); - }); - - eventSource.addEventListener('error', (err) => { - this._postEndpoint.cancel(); - this._proxy.$onDidChangeState(this._id, { - state: McpConnectionState.Kind.Error, - message: `Error connecting to ${launch.uri}: ${err.code || 0} ${err.message || JSON.stringify(err)}`, - }); - eventSource.close(); - }); + this._register(toDisposable(() => { + this._abortCtrl.abort(); + this._cts.dispose(true); + })); + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Running }); } async send(message: string) { - // only the sending of the request needs to be sequenced try { - const res = await this._requestSequencer.queue(async () => { - const { transport, url } = await this._postEndpoint.p; - const asBytes = new TextEncoder().encode(message); + await this._requestSequencer.queue(() => { + if (this._mode.value === HttpMode.SSE) { + return this._sendLegacySSE(this._mode.endpoint, message); + } else { + return this._sendStreamableHttp(message, this._mode.value === HttpMode.Http ? this._mode.sessionId : undefined); + } + }); + } catch (err) { + const msg = `Error sending message to ${this._launch.uri}: ${String(err)}`; + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: msg }); + } + } - return fetch(url, { - method: 'POST', - headers: { - ...Object.fromEntries(transport.headers), - 'Content-Type': 'application/json', - 'Content-Length': String(asBytes.length), - }, - body: asBytes, + /** + * Sends a streamable-HTTP request. + * 1. Posts to the endpoint + * 2. Updates internal state as needed. Falls back to SSE if appropriate. + * 3. If the response body is empty, JSON, or a JSON stream, handle it appropriately. + */ + private async _sendStreamableHttp(message: string, sessionId: string | undefined) { + const asBytes = new TextEncoder().encode(message); + const headers: Record = { + ...Object.fromEntries(this._launch.headers), + 'Content-Type': 'application/json', + 'Content-Length': String(asBytes.length), + Accept: 'text/event-stream, application/json', + }; + if (sessionId) { + headers['Mcp-Session-Id'] = sessionId; + } + + const res = await fetch(this._launch.uri.toString(), { + method: 'POST', + signal: this._abortCtrl.signal, + headers, + body: asBytes, + }); + + const wasUnknown = this._mode.value === HttpMode.Unknown; + + // Mcp-Session-Id is the strongest signal that we're in streamable HTTP mode + const nextSessionId = res.headers.get('Mcp-Session-Id'); + if (nextSessionId) { + this._mode = { value: HttpMode.Http, sessionId: nextSessionId }; + } + + if (this._mode.value === HttpMode.Unknown && res.status >= 400 && res.status < 500) { + this._log(LogLevel.Info, `${res.status} status sending message to ${this._launch.uri}, will attempt to fall back to legacy SSE`); + const endpoint = await this._attachSSE(); + if (endpoint) { + this._mode = { value: HttpMode.SSE, endpoint }; + await this._sendLegacySSE(endpoint, message); + } + return; + } + + if (res.status >= 300) { + this._log(LogLevel.Warning, `${res.status} status sending message to ${this._launch.uri}: ${await this._getErrText(res)}`); + return; + } + + if (this._mode.value === HttpMode.Unknown) { + this._mode = { value: HttpMode.Http, sessionId: undefined }; + } + if (wasUnknown) { + this._attachStreamableBackchannel(); + } + + // Not awaited, we don't need to block the sequencer while we read the response + this._handleSuccessfulStreamableHttp(res); + } + + private async _handleSuccessfulStreamableHttp(res: Response) { + if (res.status === 202) { + return; // no body + } + + switch (res.headers.get('Content-Type')?.toLowerCase()) { + case 'text/event-stream': { + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } }); + + try { + await this._doSSE(parser, res); + } catch (err) { + this._log(LogLevel.Warning, `Error reading SSE stream: ${String(err)}`); + } + break; + } + case 'application/json': + this._proxy.$onDidReceiveMessage(this._id, await res.text()); + break; + default: { + const responseBody = await res.text(); + if (isJSON(responseBody)) { // try to read as JSON even if the server didn't set the content type + this._proxy.$onDidReceiveMessage(this._id, responseBody); + } else { + this._log(LogLevel.Warning, `Unexpected ${res.status} response for request: ${responseBody}`); + } + } + } + } + + /** + * Attaches the SSE backchannel that streamable HTTP servers can use + * for async notifications. This is a "MAY" support, so if the server gives + * us a 4xx code, we'll stop trying to connect.. + */ + private async _attachStreamableBackchannel() { + let lastEventId: string | undefined; + for (let retry = 0; !this._store.isDisposed; retry++) { + await timeout(Math.min(retry * 1000, 30_000), this._cts.token); + + let res: Response; + try { + const headers: Record = { + ...Object.fromEntries(this._launch.headers), + 'Accept': 'text/event-stream', + }; + + if (this._mode.value === HttpMode.Http && this._mode.sessionId !== undefined) { + headers['Mcp-Session-Id'] = this._mode.sessionId; + } + if (lastEventId) { + headers['Last-Event-ID'] = lastEventId; + } + + res = await fetch(this._launch.uri.toString(), { + method: 'GET', + signal: this._abortCtrl.signal, + headers, + }); + } catch (e) { + this._log(LogLevel.Info, `Error connecting to ${this._launch.uri} for async notifications, will retry`); + continue; + } + + if (res.status >= 400) { + this._log(LogLevel.Debug, `${res.status} status connecting to ${this._launch.uri} for async notifications; they will be disabled: ${await this._getErrText(res)}`); + return; + } + + retry = 0; + + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } + if (event.id) { + lastEventId = event.id; + } }); - if (res.status >= 300) { - this._proxy.$onDidPublishLog(this._id, LogLevel.Warning, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`); + try { + await this._doSSE(parser, res); + } catch (e) { + this._log(LogLevel.Info, `Error reading from async stream, we will reconnect: ${e}`); } - } catch (err) { - // ignored + } + } + + /** + * Starts a legacy SSE attachment, where the SSE response is the session lifetime. + * Unlike `_attachStreamableBackchannel`, this fails the server if it disconnects. + */ + private async _attachSSE(): Promise { + const postEndpoint = new DeferredPromise(); + + let res: Response; + try { + res = await fetch(this._launch.uri.toString(), { + method: 'GET', + signal: this._abortCtrl.signal, + headers: { + ...Object.fromEntries(this._launch.headers), + 'Accept': 'text/event-stream', + }, + }); + if (res.status >= 300) { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `${res.status} status connecting to ${this._launch.uri} as SSE: ${await this._getErrText(res)}` }); + return; + } + } catch (e) { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error connecting to ${this._launch.uri} as SSE: ${e}` }); + return; + } + + const parser = new SSEParser(event => { + if (event.type === 'message') { + this._proxy.$onDidReceiveMessage(this._id, event.data); + } else if (event.type === 'endpoint') { + postEndpoint.complete(new URL(event.data, this._launch.uri.toString()).toString()); + } + }); + + this._register(toDisposable(() => postEndpoint.cancel())); + this._doSSE(parser, res).catch(err => { + this._proxy.$onDidChangeState(this._id, { state: McpConnectionState.Kind.Error, message: `Error reading SSE stream: ${String(err)}` }); + }); + + return postEndpoint.p; + } + + /** + * Sends a legacy SSE message to the server. The response is always empty and + * is otherwise received in {@link _attachSSE}'s loop. + */ + private async _sendLegacySSE(url: string, message: string) { + const asBytes = new TextEncoder().encode(message); + const res = await fetch(url, { + method: 'POST', + signal: this._abortCtrl.signal, + headers: { + ...Object.fromEntries(this._launch.headers), + 'Content-Type': 'application/json', + 'Content-Length': String(asBytes.length), + }, + body: asBytes, + }); + + if (res.status >= 300) { + this._log(LogLevel.Warning, `${res.status} status sending message to ${this._postEndpoint}: ${await this._getErrText(res)}`); + } + } + + /** Generic handle to pipe a response into an SSE parser. */ + private async _doSSE(parser: SSEParser, res: Response) { + if (!res.body) { + return; + } + + const reader = res.body.getReader(); + let chunk: ReadableStreamReadResult; + do { + try { + chunk = await raceCancellationError(reader.read(), this._cts.token); + } catch (err) { + reader.cancel(); + if (this._store.isDisposed) { + return; + } else { + throw err; + } + } + + if (chunk.value) { + parser.feed(chunk.value); + } + } while (!chunk.done); + } + + private _log(level: LogLevel, message: string) { + if (!this._store.isDisposed) { + this._proxy.$onDidPublishLog(this._id, level, message); } } @@ -241,3 +450,12 @@ class McpSSEHandle extends Disposable { } } } + +function isJSON(str: string): boolean { + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } +} diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 8ec74753cd6..487f91504eb 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -591,6 +591,21 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel }); } + private _secondaryQuickDiffProvider: vscode.QuickDiffProvider | undefined = undefined; + + get secondaryQuickDiffProvider(): vscode.QuickDiffProvider | undefined { + checkProposedApiEnabled(this._extension, 'quickDiffProvider'); + return this._secondaryQuickDiffProvider; + } + + set secondaryQuickDiffProvider(secondaryQuickDiffProvider: vscode.QuickDiffProvider | undefined) { + checkProposedApiEnabled(this._extension, 'quickDiffProvider'); + + this._secondaryQuickDiffProvider = secondaryQuickDiffProvider; + const secondaryQuickDiffLabel = secondaryQuickDiffProvider?.label; + this.#proxy.$updateSourceControl(this.handle, { hasSecondaryQuickDiffProvider: !!secondaryQuickDiffProvider, secondaryQuickDiffLabel }); + } + private _historyProvider: vscode.SourceControlHistoryProvider | undefined; private readonly _historyProviderDisposable = new MutableDisposable(); @@ -944,6 +959,20 @@ export class ExtHostSCM implements ExtHostSCMShape { .then(r => r || null); } + $provideSecondaryOriginalResource(sourceControlHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise { + const uri = URI.revive(uriComponents); + this.logService.trace('ExtHostSCM#$provideSecondaryOriginalResource', sourceControlHandle, uri.toString()); + + const sourceControl = this._sourceControls.get(sourceControlHandle); + + if (!sourceControl || !sourceControl.secondaryQuickDiffProvider || !sourceControl.secondaryQuickDiffProvider.provideOriginalResource) { + return Promise.resolve(null); + } + + return asPromise(() => sourceControl.secondaryQuickDiffProvider!.provideOriginalResource!(uri, token)) + .then(r => r || null); + } + $onInputBoxValueChange(sourceControlHandle: number, value: string): Promise { this.logService.trace('ExtHostSCM#$onInputBoxValueChange', sourceControlHandle); diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index fb92adb1fc9..92ba9241bf8 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -176,7 +176,7 @@ export class ExtHostSearch implements IExtHostSearch { const query = reviveQuery(rawQuery); const engine = this.createAITextSearchManager(query, provider); - return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); + return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token, result => this._proxy.$handleKeywordResult(handle, session, result)); } $enableExtensionHostSearch(): void { } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 0b035c75e19..1c0a278c4ec 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -34,7 +34,7 @@ import * as languageSelector from '../../../editor/common/languageSelector.js'; import * as languages from '../../../editor/common/languages.js'; import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js'; import { ITextEditorOptions } from '../../../platform/editor/common/editor.js'; -import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; @@ -42,7 +42,7 @@ import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; @@ -55,7 +55,7 @@ import { TestId } from '../../contrib/testing/common/testId.js'; import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestRunProfileReference, ITestTag, TestMessageType, TestResultItem, TestRunProfileBitset, denamespaceTestTag, namespaceTestTag } from '../../contrib/testing/common/testTypes.js'; import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.js'; -import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import * as extHostProtocol from './extHost.protocol.js'; import { CommandsConverter } from './extHostCommands.js'; @@ -63,6 +63,7 @@ import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; export namespace Command { @@ -2327,10 +2328,9 @@ export namespace LanguageModelChatMessage { } }); return new types.LanguageModelToolResultPart(c.toolCallId, content, c.isError); - } else if (c.type === 'image_url') { - // No image support for LanguageModelChatMessage + } else if (c.type === 'image_url' || c.type === 'extra_data') { + // Non-stable types return undefined; - } else { return new types.LanguageModelToolCallPart(c.toolCallId, c.name, c.parameters); } @@ -2430,6 +2430,8 @@ export namespace LanguageModelChatMessage2 { }; return new types.LanguageModelDataPart(value); + } else if (c.type === 'extra_data') { + return new types.LanguageModelExtraDataPart(c.kind, c.data); } else { return new types.LanguageModelToolCallPart(c.toolCallId, c.name, c.parameters); } @@ -2502,6 +2504,12 @@ export namespace LanguageModelChatMessage2 { type: 'text', value: c.value }; + } else if (c instanceof types.LanguageModelExtraDataPart) { + return { + type: 'extra_data', + kind: c.kind, + data: c.data + } satisfies chatProvider.IChatMessagePart; } else { if (typeof c !== 'string') { throw new Error('Unexpected chat message content type llm 2'); @@ -2665,6 +2673,15 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatResponseExtensionsPart { + export function from(part: vscode.ChatResponseExtensionsPart): Dto { + return { + kind: 'extensions', + extensions: part.extensions + }; + } +} + export namespace ChatResponseMovePart { export function from(part: vscode.ChatResponseMovePart): Dto { return { @@ -2824,7 +2841,7 @@ export namespace ChatResponseCodeCitationPart { export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseNotebookEditPart | vscode.ChatResponseExtensionsPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2853,6 +2870,8 @@ export namespace ChatResponsePart { return ChatResponseCodeCitationPart.from(part); } else if (part instanceof types.ChatResponseMovePart) { return ChatResponseMovePart.from(part); + } else if (part instanceof types.ChatResponseExtensionsPart) { + return ChatResponseExtensionsPart.from(part); } return { @@ -2888,10 +2907,11 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined, extension: IRelaxedExtensionDescription): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.kind === 'tool'); const variableReferences = request.variables.variables.filter(v => v.kind !== 'tool'); - const requestWithoutId = { + const requestWithAllProps: vscode.ChatRequest = { + id: request.requestId, prompt: request.message, command: request.command, attempt: request.attempt ?? 0, @@ -2905,16 +2925,28 @@ export namespace ChatAgentRequest { location2, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, tools, - model + model, + editedFileEvents: request.editedFileEvents, }; - if (request.requestId) { - return { - ...requestWithoutId, - id: request.requestId - }; + + if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + delete (requestWithAllProps as any).id; + delete (requestWithAllProps as any).attempt; + delete (requestWithAllProps as any).enableCommandDetection; + delete (requestWithAllProps as any).isParticipantDetected; + delete (requestWithAllProps as any).location; + delete (requestWithAllProps as any).location2; + delete (requestWithAllProps as any).editedFileEvents; } - // This cast is done to allow sending the stabl version of ChatRequest which does not have an id property - return requestWithoutId as unknown as vscode.ChatRequest; + + if (!isProposedApiEnabled(extension, 'chatParticipantAdditions')) { + delete requestWithAllProps.acceptedConfirmationData; + delete requestWithAllProps.rejectedConfirmationData; + delete (requestWithAllProps as any).tools; + } + + + return requestWithAllProps; } } @@ -3307,3 +3339,28 @@ export namespace IconPath { return iconPath; } } + +export namespace McpServerDefinition { + function isHttpConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpHttpServerDefinition { + return !!(candidate as vscode.McpHttpServerDefinition).uri; + } + + export function from(item: vscode.McpServerDefinition): McpServerLaunch.Serialized { + return McpServerLaunch.toSerialized( + isHttpConfig(item) + ? { + type: McpServerTransportType.HTTP, + uri: item.uri, + headers: Object.entries(item.headers), + } + : { + type: McpServerTransportType.Stdio, + cwd: item.cwd, + args: item.args, + command: item.command, + env: item.env, + envFile: undefined, + } + ); + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d07bd22d0a9..a44b634aef8 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4525,6 +4525,12 @@ export enum ChatEditingSessionActionOutcome { Saved = 3 } +export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, +} + //#endregion //#region Interactive Editor @@ -4680,6 +4686,13 @@ export class ChatResponseMovePart { } } +export class ChatResponseExtensionsPart { + constructor( + public readonly extensions: string[], + ) { + } +} + export class ChatResponseTextEditPart implements vscode.ChatResponseTextEditPart { uri: vscode.Uri; edits: vscode.TextEdit[]; @@ -4711,13 +4724,14 @@ export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebook } } -export class ChatRequestTurn implements vscode.ChatRequestTurn { +export class ChatRequestTurn implements vscode.ChatRequestTurn2 { constructor( readonly prompt: string, readonly command: string | undefined, readonly references: vscode.ChatPromptReference[], readonly participant: string, - readonly toolReferences: vscode.ChatLanguageModelToolReference[] + readonly toolReferences: vscode.ChatLanguageModelToolReference[], + readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[] ) { } } @@ -4847,27 +4861,6 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage return this._content; } - // Temp to avoid breaking changes - set content2(value: (string | LanguageModelToolResultPart | LanguageModelToolCallPart)[] | undefined) { - if (value) { - this.content = value.map(part => { - if (typeof part === 'string') { - return new LanguageModelTextPart(part); - } - return part; - }); - } - } - - get content2(): (string | LanguageModelToolResultPart | LanguageModelToolCallPart)[] | undefined { - return this.content.map(part => { - if (part instanceof LanguageModelTextPart) { - return part.value; - } - return part; - }); - } - name: string | undefined; constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart)[], name?: string) { @@ -4877,22 +4870,21 @@ export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage } } - export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessage2 { - static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage2 { + static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string): LanguageModelChatMessage2 { return new LanguageModelChatMessage2(LanguageModelChatMessageRole.User, content, name); } - static Assistant(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage2 { + static Assistant(content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string): LanguageModelChatMessage2 { return new LanguageModelChatMessage2(LanguageModelChatMessageRole.Assistant, content, name); } role: vscode.LanguageModelChatMessageRole; - private _content: (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[] = []; + private _content: (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] = []; - set content(value: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[]) { + set content(value: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[]) { if (typeof value === 'string') { // we changed this and still support setting content with a string property. this keep the API runtime stable // despite the breaking change in the type definition. @@ -4902,7 +4894,7 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } - get content(): (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[] { + get content(): (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] { return this._content; } @@ -4918,7 +4910,7 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } - get content2(): (string | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[] | undefined { + get content2(): (string | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[] | undefined { return this.content.map(part => { if (part instanceof LanguageModelTextPart) { return part.value; @@ -4929,7 +4921,7 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag name: string | undefined; - constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string) { + constructor(role: vscode.LanguageModelChatMessageRole, content: string | (LanguageModelTextPart | LanguageModelToolResultPart2 | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelExtraDataPart)[], name?: string) { this.role = role; this.content = content; this.name = name; @@ -4980,6 +4972,24 @@ export class LanguageModelDataPart implements vscode.LanguageModelDataPart { } } +export class LanguageModelExtraDataPart implements vscode.LanguageModelExtraDataPart { + kind: string; + data: any; + + constructor(kind: string, data: any) { + this.kind = kind; + this.data = data; + } + + toJSON() { + return { + $mid: MarshalledId.LanguageModelExtraDataPart, + kind: this.kind, + data: this.data, + }; + } +} + /** * Enum for supported image MIME types. */ @@ -5171,15 +5181,17 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition public label: string, public command: string, public args: string[], - public env: Record + public env: Record, + public version?: string, ) { } } -export class McpSSEServerDefinition implements vscode.McpSSEServerDefinition { - headers: [string, string][] = []; +export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { constructor( public label: string, - public uri: URI + public uri: URI, + public headers: Record = {}, + public version?: string, ) { } } //#endregion diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 1d386e6402e..b93ce34773d 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -945,7 +945,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac // --- encodings --- - async decode(content: Uint8Array, args: { uri?: vscode.Uri; encoding?: string }): Promise { + async decode(content: Uint8Array, args?: { uri?: vscode.Uri; encoding?: string }): Promise { const [uri, opts] = this.toEncodeDecodeParameters(args); const options = await this._proxy.$resolveDecoding(uri, opts); @@ -967,7 +967,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return consumeStream(stream, chunks => chunks.join('')); } - async encode(content: string, args: { uri?: vscode.Uri; encoding?: string }): Promise { + async encode(content: string, args?: { uri?: vscode.Uri; encoding?: string }): Promise { const [uri, options] = this.toEncodeDecodeParameters(args); const { encoding, addBOM } = await this._proxy.$resolveEncoding(uri, options); @@ -981,9 +981,9 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return readableToBuffer(res).buffer; } - private toEncodeDecodeParameters(opts: { uri?: vscode.Uri; encoding?: string }): [UriComponents | undefined, { encoding: string } | undefined] { - const uri = isUriComponents(opts.uri) ? opts.uri : undefined; - const encoding = typeof opts.encoding === 'string' ? opts.encoding : undefined; + private toEncodeDecodeParameters(opts?: { uri?: vscode.Uri; encoding?: string }): [UriComponents | undefined, { encoding: string } | undefined] { + const uri = isUriComponents(opts?.uri) ? opts.uri : undefined; + const encoding = typeof opts?.encoding === 'string' ? opts.encoding : undefined; return [uri, encoding ? { encoding } : undefined]; } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 331d9a7b180..db7afe10529 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -28,7 +28,7 @@ import { ISignService } from '../../../platform/sign/common/sign.js'; import { SignService } from '../../../platform/sign/node/signService.js'; import { ExtHostTelemetry, IExtHostTelemetry } from '../common/extHostTelemetry.js'; import { IExtHostMpcService } from '../common/extHostMcp.js'; -import { NodeExtHostMpcService } from './extHostMpcNode.js'; +import { NodeExtHostMpcService } from './extHostMcpNode.js'; // ######################################################################### // ### ### diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index bb381ed9504..edc1452c76d 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -253,7 +253,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`); } if (mode === 'esm') { - r = await import(module.fsPath); + r = await import(module.toString(true)); } else { r = require(module.fsPath); } diff --git a/src/vs/workbench/api/node/extHostMpcNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts similarity index 100% rename from src/vs/workbench/api/node/extHostMpcNode.ts rename to src/vs/workbench/api/node/extHostMcpNode.ts diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index d5fc283aeb6..bb5ff5e46ff 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -6,10 +6,10 @@ import assert from 'assert'; import { MainThreadMessageService } from '../../browser/mainThreadMessageService.js'; import { IDialogService, IPrompt, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; -import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, INotificationSource, INotificationSourceFilter, NotificationsFilter, IStatusHandle } from '../../../../platform/notification/common/notification.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { mock } from '../../../../base/test/common/mock.js'; -import { IDisposable, Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; @@ -44,8 +44,8 @@ const emptyNotificationService = new class implements INotificationService { prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { throw new Error('not implemented'); } - status(message: string | Error, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string | Error, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } setFilter(): void { throw new Error('not implemented'); @@ -87,8 +87,8 @@ class EmptyNotificationService implements INotificationService { prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { throw new Error('Method not implemented'); } - status(message: string, options?: IStatusMessageOptions): IDisposable { - return Disposable.None; + status(message: string, options?: IStatusMessageOptions): IStatusHandle { + return { close: () => { } }; } setFilter(): void { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 96c55513d54..8cd81152eac 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -776,22 +776,6 @@ suite('ExtHostTypes', function () { assert.throws(() => types.FileDecoration.validate({ badge: 'ããã' })); }); - test('No longer possible to set content on LanguageModelChatMessage', function () { - const m = types.LanguageModelChatMessage.Assistant(''); - m.content = [new types.LanguageModelToolCallPart('toolCall.call.callId', 'toolCall.tool.name', 'toolCall.call.parameters')]; - - assert.equal(m.content.length, 1); - assert.equal(m.content2?.length, 1); - - - m.content2 = ['foo']; - assert.equal(m.content.length, 1); - assert.ok(m.content[0] instanceof types.LanguageModelTextPart); - - assert.equal(m.content2?.length, 1); - assert.ok(typeof m.content2[0] === 'string'); - }); - test('runtime stable, type-def changed', function () { // see https://github.com/microsoft/vscode/issues/231938 const m = new types.LanguageModelChatMessage(types.LanguageModelChatMessageRole.User, []); diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 94380c10ec4..4abd86979a5 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -26,6 +26,7 @@ import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteSt import { TextSearchManager } from '../../../services/search/common/textSearchManager.js'; import { NativeTextSearchManager } from '../../../services/search/node/textSearchManager.js'; import type * as vscode from 'vscode'; +import { AISearchKeyword } from '../../../services/search/common/searchExtTypes.js'; let rpcProtocol: TestRPCProtocol; let extHostSearch: NativeExtHostSearch; @@ -36,6 +37,8 @@ class MockMainThreadSearch implements MainThreadSearchShape { results: Array = []; + keywords: Array = []; + $registerFileSearchProvider(handle: number, scheme: string): void { this.lastHandle = handle; } @@ -59,6 +62,10 @@ class MockMainThreadSearch implements MainThreadSearchShape { this.results.push(...data); } + $handleKeywordResult(handle: number, session: number, data: AISearchKeyword): void { + this.keywords.push(data); + } + $handleTelemetry(eventName: string, data: any): void { } diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index f6ee0ded48e..2487213aa2d 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -16,7 +16,6 @@ import { ServicesAccessor } from '../../../platform/instantiation/common/instant import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; -import { IQuickInputService } from '../../../platform/quickinput/common/quickInput.js'; import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js'; class KeybindingsReferenceAction extends Action2 { @@ -336,7 +335,6 @@ class GetStartedWithAccessibilityFeatures extends Action2 { class AskVSCodeCopilot extends Action2 { static readonly ID = 'workbench.action.askVScode'; - // add check for enablement constructor() { super({ id: AskVSCodeCopilot.ID, @@ -348,15 +346,8 @@ class AskVSCodeCopilot extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const quickInputService = accessor.get(IQuickInputService); const commandService = accessor.get(ICommandService); - const input = await quickInputService.input({ - title: localize('askVscodeTitle', "Ask @vscode"), - placeHolder: localize('askVscodePlaceholder', "@vscode can help you with settings, commands, or how to do something in VS Code.") - }); - if (input) { - commandService.executeCommand('workbench.action.chat.open', { mode: 'ask', query: `@vscode ${input}` }); - } + commandService.executeCommand('workbench.action.chat.open', { mode: 'ask', query: '@vscode ', isPartialQuery: true }); } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 351252cecda..838d74fd56f 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -290,6 +290,7 @@ export class BreadcrumbsControl { dispose(): void { this._disposables.dispose(); this._breadcrumbsDisposables.dispose(); + this._model.dispose(); this._ckBreadcrumbsPossible.reset(); this._ckBreadcrumbsVisible.reset(); this._ckBreadcrumbsActive.reset(); diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index dc3f83afe6c..df972d58710 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -16,6 +16,7 @@ import { TextDiffEditor } from './textDiffEditor.js'; import { ActiveCompareEditorCanSwapContext, TextCompareEditorActiveContext, TextCompareEditorVisibleContext } from '../../../common/contextkeys.js'; import { DiffEditorInput } from '../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IUntypedEditorInput } from '../../../common/editor.js'; export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; @@ -150,14 +151,14 @@ export function registerDiffEditorCommands(): void { // yet opened. This ensures that the swapping is not // bringing up a confirmation dialog to save. if (diffInput.modified.isModified() && editorService.findEditors({ resource: diffInput.modified.resource, typeId: diffInput.modified.typeId, editorId: diffInput.modified.editorId }).length === 0) { - await editorService.openEditor({ - ...untypedDiffInput.modified, - options: { - ...untypedDiffInput.modified.options, - pinned: true, - inactive: true - } - }, activeGroup); + const editorToOpen: IUntypedEditorInput = { ...untypedDiffInput.modified }; + if (!editorToOpen.options) { + editorToOpen.options = {}; + } + editorToOpen.options.pinned = true; + editorToOpen.options.inactive = true; + + await editorService.openEditor(editorToOpen, activeGroup); } // Replace the input with the swapped variant diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 80de2a657b9..a21f003405a 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -17,7 +17,19 @@ transition: background-color 0.15s ease-out; } -.monaco-workbench .part.statusbar.status-border-top::after { +.monaco-workbench.mac:not(.fullscreen) .part.statusbar:focus { + /* Rounded corners to make focus outline appear properly (unless fullscreen) */ + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} +.monaco-workbench.mac:not(.fullscreen).macos-bigsur-or-newer .part.statusbar:focus { + /* macOS Big Sur increased rounded corners size */ + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; +} + +.monaco-workbench .part.statusbar:not(:focus).status-border-top::after { + /* Top border only visible unless focused to make room for focus outline */ content: ''; position: absolute; top: 0; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 4cb6b8f4000..e601d324307 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -188,6 +188,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { persistence: { hideOnKeyDown: true, sticky: focus + }, + appearance: { + maxHeightRatio: 0.9 } } ))); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 72f5b13eef8..24f54228122 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -73,10 +73,10 @@ export interface IWorkbench { retrievePerformanceMarks(): Promise<[string, readonly PerformanceMark[]][]>; /** - * Allows to open a `URI` with the standard opener service of the + * Allows to open a target Uri with the standard opener service of the * workbench. */ - openUri(target: URI): Promise; + openUri(target: URI | UriComponents): Promise; }; window: { @@ -755,6 +755,7 @@ export interface ISettingsSyncOptions { * Authentication provider */ readonly authenticationProvider?: { + /** * Unique identifier of the authentication provider. */ @@ -801,6 +802,7 @@ export interface IDevelopmentOptions { * when remote resolvers are used in the web. */ export interface IRemoteResourceProvider { + /** * Path the workbench should delegate requests to. The embedder should * install a service worker on this path and emit {@link onDidReceiveRequest} @@ -819,6 +821,7 @@ export interface IRemoteResourceProvider { * headers, but for now we only deal with GET requests. */ export interface IRemoteResourceRequest { + /** * Request URI. Generally will begin with the current * origin and {@link IRemoteResourceProvider.pathPrefix}. diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 422fbba83e7..0ebe2713def 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -5,7 +5,7 @@ import { ITunnel, ITunnelOptions, IWorkbench, IWorkbenchConstructionOptions, Menu } from './web.api.js'; import { BrowserMain } from './web.main.js'; -import { URI } from '../../base/common/uri.js'; +import { URI, UriComponents } from '../../base/common/uri.js'; import { IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { CommandsRegistry } from '../../platform/commands/common/commands.js'; import { mark, PerformanceMark } from '../../base/common/performance.js'; @@ -125,10 +125,10 @@ export namespace env { /** * {@linkcode IWorkbench.env IWorkbench.env.openUri} */ - export async function openUri(target: URI): Promise { + export async function openUri(target: URI | UriComponents): Promise { const workbench = await workbenchPromise.p; - return workbench.env.openUri(target); + return workbench.env.openUri(URI.isUri(target) ? target : URI.from(target)); } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 5dcdad8aaac..8aec734eb49 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -27,7 +27,7 @@ import { IAnyWorkspaceIdentifier, IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW import { IWorkbenchConfigurationService } from '../services/configuration/common/configuration.js'; import { onUnexpectedError } from '../../base/common/errors.js'; import { setFullscreen } from '../../base/browser/browser.js'; -import { URI } from '../../base/common/uri.js'; +import { URI, UriComponents } from '../../base/common/uri.js'; import { WorkspaceService } from '../services/configuration/browser/configurationService.js'; import { ConfigurationCache } from '../services/configuration/common/configurationCache.js'; import { ISignService } from '../../platform/sign/common/sign.js'; @@ -182,8 +182,8 @@ export class BrowserMain extends Disposable { return timerService.getPerformanceMarks(); }, - async openUri(uri: URI): Promise { - return openerService.open(uri, {}); + async openUri(uri: URI | UriComponents): Promise { + return openerService.open(URI.isUri(uri) ? uri : URI.from(uri), {}); } }, logger: { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 028d91f065d..60578ec0682 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -537,6 +537,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'type': 'string', 'enum': ['hidden', 'visibleInWorkspace', 'visible'], 'default': 'hidden', + 'tags': ['onExp'], 'description': localize('secondarySideBarDefaultVisibility', "Controls the default visibility of the secondary side bar in workspaces or empty windows opened for the first time."), 'enumDescriptions': [ localize('workbench.secondarySideBar.defaultVisibility.hidden', "The secondary side bar is hidden by default."), diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index a3e8516c6e6..3db16dd0d94 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -410,7 +410,7 @@ export interface IFileEditorFactory { typeId: string; /** - * Creates new new editor capable of showing files. + * Creates new editor capable of showing files. */ createFileEditor(resource: URI, preferredResource: URI | undefined, preferredName: string | undefined, preferredDescription: string | undefined, preferredEncoding: string | undefined, preferredLanguageId: string | undefined, preferredContents: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; @@ -504,12 +504,12 @@ export interface IResourceSideBySideEditorInput extends IBaseUntypedEditorInput /** * The right hand side editor to open inside a side-by-side editor. */ - readonly primary: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly primary: Omit | Omit | Omit; /** * The left hand side editor to open inside a side-by-side editor. */ - readonly secondary: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly secondary: Omit | Omit | Omit; } /** @@ -524,12 +524,25 @@ export interface IResourceDiffEditorInput extends IBaseUntypedEditorInput { /** * The left hand side editor to open inside a diff editor. */ - readonly original: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly original: Omit | Omit | Omit; /** * The right hand side editor to open inside a diff editor. */ - readonly modified: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; + readonly modified: Omit | Omit | Omit; +} + +export interface ITextResourceDiffEditorInput extends IBaseTextResourceEditorInput { + + /** + * The left hand side text editor to open inside a diff editor. + */ + readonly original: Omit | Omit; + + /** + * The right hand side text editor to open inside a diff editor. + */ + readonly modified: Omit | Omit; } /** @@ -558,7 +571,7 @@ export interface IResourceMultiDiffEditorInput extends IBaseUntypedEditorInput { export interface IMultiDiffEditorResource extends IResourceDiffEditorInput { readonly goToFileResource?: URI; } -export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; +export type IResourceMergeEditorInputSide = (Omit | Omit) & { detail?: string }; /** * A resource merge editor input compares multiple editors @@ -582,12 +595,12 @@ export interface IResourceMergeEditorInput extends IBaseUntypedEditorInput { /** * The base common ancestor of the file to merge. */ - readonly base: IResourceEditorInput | ITextResourceEditorInput; + readonly base: Omit | Omit; /** * The resulting output of the merge. */ - readonly result: IResourceEditorInput | ITextResourceEditorInput; + readonly result: Omit | Omit; } export function isResourceEditorInput(editor: unknown): editor is IResourceEditorInput { diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 945c927bcb7..08861595cb0 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource } from '../../platform/notification/common/notification.js'; +import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource, IStatusHandle } from '../../platform/notification/common/notification.js'; import { toErrorMessage, isErrorWithActions } from '../../base/common/errorMessage.js'; import { Event, Emitter } from '../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; +import { Disposable } from '../../base/common/lifecycle.js'; import { isCancellationError } from '../../base/common/errors.js'; import { Action } from '../../base/common/actions.js'; import { equals } from '../../base/common/arrays.js'; @@ -35,7 +35,7 @@ export interface INotificationsModel { readonly onDidChangeStatusMessage: Event; - showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable; + showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle; //#endregion } @@ -275,24 +275,23 @@ export class NotificationsModel extends Disposable implements INotificationsMode return item; } - showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable { + showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle { const item = StatusMessageViewItem.create(message, options); if (!item) { - return Disposable.None; + return { close: () => { } }; } - // Remember as current status message and fire events this._statusMessage = item; this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item }); - return toDisposable(() => { - - // Only reset status message if the item is still the one we had remembered - if (this._statusMessage === item) { - this._statusMessage = undefined; - this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); + return { + close: () => { + if (this._statusMessage === item) { + this._statusMessage = undefined; + this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item }); + } } - }); + }; } } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 0f1c1bbcdcb..77bc44df9cf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -290,6 +290,20 @@ const configuration: IConfigurationNode = { } } }, + 'accessibility.signals.nextEditSuggestion': { + ...signalFeatureBase, + 'description': localize('accessibility.signals.nextEditSuggestion', "Plays a signal - sound / audio cue and/or announcement (alert) when there is a next edit suggestion."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.nextEditSuggestion.sound', "Plays a sound when there is a next edit suggestion."), + ...soundFeatureBase, + }, + 'announcement': { + 'description': localize('accessibility.signals.nextEditSuggestion.announcement', "Announces when there is a next edit suggestion."), + ...announcementFeatureBase, + }, + } + }, 'accessibility.signals.lineHasError': { ...signalFeatureBase, 'description': localize('accessibility.signals.lineHasError', "Plays a signal - sound (audio cue) and/or announcement (alert) - when the active line has an error."), diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 985863d7d57..3e1df623987 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -99,7 +99,7 @@ export interface IChatViewOpenRequestEntry { response: string; } -export const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog'; +const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog'; export function registerChatActions() { registerAction2(class OpenChatGlobalAction extends Action2 { @@ -111,7 +111,7 @@ export function registerChatActions() { icon: Codicon.copilot, f1: true, category: CHAT_CATEGORY, - precondition: ChatContextKeys.Setup.hidden.toNegated(), + precondition: ChatContextKeys.Setup.hidden.negate(), keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, @@ -607,12 +607,12 @@ export function registerChatActions() { } }); - registerAction2(class ShowLimitReachedDialogAction extends Action2 { + registerAction2(class ShowQuotaExceededDialogAction extends Action2 { constructor() { super({ id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, - title: localize('upgradeChat', "Upgrade to Copilot Pro") + title: localize('upgradeChat', "Upgrade Copilot Plan") }); } @@ -622,30 +622,36 @@ export function registerChatActions() { const dialogService = accessor.get(IDialogService); const telemetryService = accessor.get(ITelemetryService); - const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); - let message: string; - const { chatQuotaExceeded, completionsQuotaExceeded } = chatEntitlementService.quotas; + const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; + const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0; if (chatQuotaExceeded && !completionsQuotaExceeded) { - message = localize('chatQuotaExceeded', "You've run out of free chat messages. You still have free code completions available in the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('chatQuotaExceeded', "You've reached your monthly chat requests quota. You still have free code completions available."); } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - message = localize('completionsQuotaExceeded', "You've run out of free code completions. You still have free chat messages available in the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat requests available."); } else { - message = localize('chatAndCompletionsQuotaExceeded', "You've reached the limit of the Copilot Free plan. These limits will reset on {0}.", dateFormatter.format(chatEntitlementService.quotas.quotaResetDate)); + message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat requests and code completions quota."); } - const upgradeToPro = localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to additional models"); + if (chatEntitlementService.quotas.resetDate) { + const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); + const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate); + message = [message, localize('quotaResetDate', "The allowance will renew on {0}.", dateFormatter.format(quotaResetDate))].join(' '); + } + + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; + const upgradeToPro = limited ? localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited basic chat requests\n- Access to premium models") : undefined; await dialogService.prompt({ type: 'none', - message: localize('copilotFree', "Copilot Limit Reached"), + message: localize('copilotQuotaReached', "Copilot Quota Reached"), cancelButton: { label: localize('dismiss', "Dismiss"), run: () => { /* noop */ } }, buttons: [ { - label: localize('upgradePro', "Upgrade to Copilot Pro"), + label: limited ? localize('upgradePro', "Upgrade to Copilot Pro") : localize('upgradePlan', "Upgrade Copilot Plan"), run: () => { const commandId = 'workbench.action.chat.upgradePlan'; telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' }); @@ -655,10 +661,10 @@ export function registerChatActions() { ], custom: { icon: Codicon.copilotWarningLarge, - markdownDetails: [ + markdownDetails: coalesce([ { markdown: new MarkdownString(message, true) }, - { markdown: new MarkdownString(upgradeToPro, true) } - ] + upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined + ]) } }); } @@ -720,8 +726,8 @@ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { localize('toggle.chatControl', 'Copilot Controls'), localize('toggle.chatControlsDescription', "Toggle visibility of the Copilot Controls in title bar"), 5, ContextKeyExpr.and( - ChatContextKeys.Setup.hidden.toNegated(), - IsCompactTitleBarContext.toNegated(), + ChatContextKeys.Setup.hidden.negate(), + IsCompactTitleBarContext.negate(), ChatContextKeys.supported ) ); @@ -766,8 +772,9 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben }); const chatExtensionInstalled = chatEntitlementService.sentiment === ChatSentiment.Installed; - const { chatQuotaExceeded, completionsQuotaExceeded } = chatEntitlementService.quotas; + const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; let primaryActionId = TOGGLE_CHAT_ACTION_ID; let primaryActionTitle = localize('toggleChat', "Toggle Chat"); @@ -777,15 +784,9 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben primaryActionId = CHAT_SETUP_ACTION_ID; primaryActionTitle = localize('signInToChatSetup', "Sign in to use Copilot..."); primaryActionIcon = Codicon.copilotNotConnected; - } else if (chatQuotaExceeded || completionsQuotaExceeded) { + } else if (chatQuotaExceeded && limited) { primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - if (chatQuotaExceeded && !completionsQuotaExceeded) { - primaryActionTitle = localize('chatQuotaExceededButton', "Monthly chat messages limit reached. Click for details."); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - primaryActionTitle = localize('completionsQuotaExceededButton', "Monthly code completions limit reached. Click for details."); - } else { - primaryActionTitle = localize('chatAndCompletionsQuotaExceededButton', "Copilot Free plan limit reached. Click for details."); - } + primaryActionTitle = localize('chatQuotaExceededButton', "Copilot Free plan chat requests quota reached. Click for details."); primaryActionIcon = Codicon.copilotWarning; } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 96ebfeb4350..6be85d4c04b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { groupBy } from '../../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../../../base/common/keybindings.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { isElectron } from '../../../../../base/common/platform.js'; -import { basename, dirname } from '../../../../../base/common/resources.js'; -import { compare } from '../../../../../base/common/strings.js'; +import { basename, dirname, extUri } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { WithUriValue } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -20,7 +21,7 @@ import { Command, SymbolKinds } from '../../../../../editor/common/languages.js' import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; @@ -30,6 +31,7 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js'; @@ -55,16 +57,16 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, OmittedState } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { ChatAgentLocation } from '../../common/constants.js'; -import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js'; import { imageToHash, isImage } from '../chatPasteProviders.js'; import { isQuickChat } from '../chatWidget.js'; -import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js'; +import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js'; import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js'; import { resizeImage } from '../imageUtils.js'; -import { COMMAND_ID as USE_PROMPT_COMMAND_ID } from '../promptSyntax/contributions/usePromptCommand.js'; +import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/attachInstructionsCommand.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { runAttachPromptAction, registerReusablePromptActions } from './reusablePromptActions/index.js'; +import { runAttachInstructionsAction, registerPromptActions } from './promptActions/index.js'; export function registerChatContextActions() { registerAction2(AttachContextAction); @@ -77,9 +79,33 @@ export function registerChatContextActions() { /** * We fill the quickpick with these types, and enable some quick access providers */ -type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | - IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | - IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IReusablePromptQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem; +type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem + | IToolsQuickPickItem | IToolQuickPickItem + | IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem + | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem + | IFolderQuickPickItem | IFolderResultQuickPickItem + | IDiagnosticsQuickPickItem | IDiagnosticsQuickPickItemWithFilter; + +function isIAttachmentQuickPickItem(obj: unknown): obj is IAttachmentQuickPickItem { + return ( + typeof obj === 'object' + && obj !== null + && typeof (obj).kind === 'string' + ); +} + +const attachmentsOrdinals: (IAttachmentQuickPickItem['kind'])[] = [ + // bottom-most + 'tools', + 'screenshot', + 'image', + 'quickaccess', + 'diagnostic', + 'instructions', + 'folder', + 'open-editors', + // top-most +]; /** * These are the types that we can get out of the quick pick @@ -101,18 +127,6 @@ function isISymbolQuickPickItem(obj: unknown): obj is ISymbolQuickPickItem { && !!(obj as ISymbolQuickPickItem).symbol); } -function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IFolderResultQuickPickItem).kind === 'folder-search-result'); -} - -function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter { - return ( - typeof obj === 'object' - && (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter'); -} - function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource { return ( typeof obj === 'object' @@ -120,40 +134,11 @@ function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithRe && URI.isUri((obj as IQuickPickItemWithResource).resource)); } -function isIOpenEditorsQuickPickItem(obj: unknown): obj is IOpenEditorsQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IOpenEditorsQuickPickItem).id === 'open-editors'); -} -function isISearchResultsQuickPickItem(obj: unknown): obj is ISearchResultsQuickPickItem { - return ( - typeof obj === 'object' - && (obj as ISearchResultsQuickPickItem).kind === 'search-results'); -} - -function isScreenshotQuickPickItem(obj: unknown): obj is IScreenShotQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IScreenShotQuickPickItem).kind === 'screenshot'); -} - -function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPickItem { - return ( - typeof obj === 'object' - && (obj as IRelatedFilesQuickPickItem).kind === 'related-files' - ); -} - -/** - * Checks is a provided object is a prompt instructions quick pick item. - */ -function isPromptInstructionsQuickPickItem(obj: unknown): obj is IReusablePromptQuickPickItem { - if (!obj || typeof obj !== 'object') { - return false; - } - - return ('kind' in obj && obj.kind === 'reusable-prompt'); +interface IToolsQuickPickItem extends IQuickPickItem { + kind: 'tools'; + id: string; + label: string; } interface IRelatedFilesQuickPickItem extends IQuickPickItem { @@ -185,7 +170,6 @@ interface ICommandVariableQuickPickItem extends IQuickPickItem { command: Command; name?: string; value: unknown; - icon?: ThemeIcon; } @@ -194,6 +178,7 @@ interface IToolQuickPickItem extends IQuickPickItem { id: string; name?: string; icon?: ThemeIcon; + tool: IToolData; } interface IQuickAccessQuickPickItem extends IQuickPickItem { @@ -234,19 +219,19 @@ interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem { } /** - * Quick pick item for reusable prompt attachment. + * Quick pick item for instructions attachment. */ -const REUSABLE_PROMPT_PICK_ID = 'reusable-prompt'; -interface IReusablePromptQuickPickItem extends IQuickPickItem { +const INSTRUCTION_PICK_ID = 'instructions'; +interface IInstructionsQuickPickItem extends IQuickPickItem { /** * The ID of the quick pick item. */ - id: typeof REUSABLE_PROMPT_PICK_ID; + id: typeof INSTRUCTION_PICK_ID; /** - * Unique kind identifier of the reusable prompt attachment. + * Unique kind identifier of the instructions attachment. */ - kind: typeof REUSABLE_PROMPT_PICK_ID; + kind: typeof INSTRUCTION_PICK_ID; /** * Keybinding of the command. @@ -408,12 +393,12 @@ class AttachSelectionToChatAction extends Action2 { } export class AttachSearchResultAction extends Action2 { - static readonly Name = 'searchResults'; - static readonly ID = 'workbench.action.chat.insertSearchResults'; + + private static readonly Name = 'searchResults'; constructor() { super({ - id: AttachSearchResultAction.ID, + id: 'workbench.action.chat.insertSearchResults', title: localize2('chat.insertSearchResults', 'Add Search Results to Chat'), category: CHAT_CATEGORY, f1: false, @@ -427,9 +412,9 @@ export class AttachSearchResultAction extends Action2 { }] }); } - async run(accessor: ServicesAccessor, ...args: any[]) { + async run(accessor: ServicesAccessor) { const logService = accessor.get(ILogService); - const widget = (await showChatView(accessor.get(IViewsService))); + const widget = await showChatView(accessor.get(IViewsService)); if (!widget) { logService.trace('InsertSearchResultAction: no chat view available'); @@ -445,7 +430,7 @@ export class AttachSearchResultAction extends Action2 { } let insertText = `#${AttachSearchResultAction.Name}`; - const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length); + const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length); // check character before the start of the range. If it's not a space, add a space const model = editor.getModel(); if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') { @@ -461,26 +446,24 @@ export class AttachSearchResultAction extends Action2 { export class AttachContextAction extends Action2 { - static readonly ID = 'workbench.action.chat.attachContext'; - - constructor(desc: Readonly = { - id: AttachContextAction.ID, - title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."), - icon: Codicon.attach, - category: CHAT_CATEGORY, - keybinding: { - when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)), - primary: KeyMod.CtrlCmd | KeyCode.Slash, - weight: KeybindingWeight.EditorContrib - }, - menu: { - when: ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), - id: MenuId.ChatInputAttachmentToolbar, - group: 'navigation', - order: 3 - }, - }) { - super(desc); + constructor() { + super({ + id: 'workbench.action.chat.attachContext', + title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."), + icon: Codicon.attach, + category: CHAT_CATEGORY, + keybinding: { + when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)), + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + }, + menu: { + when: ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), + id: MenuId.ChatInputAttachmentToolbar, + group: 'navigation', + order: 3 + }, + }); } private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { @@ -493,10 +476,136 @@ export class AttachContextAction extends Action2 { `:${item.range.startLineNumber}`); } - private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + private async _attachContext(accessor: ServicesAccessor, widget: IChatWidget, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) { + const commandService = accessor.get(ICommandService); + const clipboardService = accessor.get(IClipboardService); + const editorService = accessor.get(IEditorService); + const labelService = accessor.get(ILabelService); + const viewsService = accessor.get(IViewsService); + const chatEditingService = accessor.get(IChatEditingService); + const hostService = accessor.get(IHostService); + const fileService = accessor.get(IFileService); + const textModelService = accessor.get(ITextModelService); + const quickInputService = accessor.get(IQuickInputService); + const toAttach: IChatRequestVariableEntry[] = []; for (const pick of picks) { - if (isISymbolQuickPickItem(pick) && pick.symbol) { + + if (isIAttachmentQuickPickItem(pick)) { + if (pick.kind === 'folder-search-result') { + toAttach.push({ + kind: 'directory', + id: pick.id, + value: pick.resource, + name: basename(pick.resource), + }); + } else if (pick.kind === 'diagnostic-filter') { + toAttach.push({ + id: pick.id, + name: pick.label, + value: pick.filter, + kind: 'diagnostic', + icon: pick.icon, + ...pick.filter, + }); + + } else if (pick.kind === 'open-editors') { + for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { + const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; + if (uri) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: uri }), + value: uri, + name: labelService.getUriBasenameLabel(uri), + }); + } + } + } else if (pick.kind === 'search-results') { + const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; + for (const result of searchView.model.searchResult.matches()) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: result.resource }), + value: result.resource, + name: labelService.getUriBasenameLabel(result.resource), + }); + } + } else if (pick.kind === 'related-files') { + // Get all provider results and show them in a second tier picker + const chatSessionId = widget.viewModel?.sessionId; + if (!chatSessionId || !chatEditingService) { + continue; + } + const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); + if (!relatedFiles) { + continue; + } + const attachments = widget.attachmentModel.getAttachmentIDs(); + const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) + .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { + acc.push({ type: 'separator', label: cur.group }); + for (const file of cur.files) { + acc.push({ + type: 'item', + label: labelService.getUriBasenameLabel(file.uri), + description: labelService.getUriLabel(dirname(file.uri), { relative: true }), + value: file.uri, + disabled: attachments.has(this._getFileContextId({ resource: file.uri })), + picked: true + }); + } + return acc; + }, [])); + const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true }); + for (const file of selectedFiles ?? []) { + toAttach.push({ + kind: 'file', + id: this._getFileContextId({ resource: file.value }), + value: file.value, + name: file.label, + omittedState: OmittedState.NotOmitted + }); + } + } else if (pick.kind === 'screenshot') { + const blob = await hostService.getScreenshot(); + if (blob) { + toAttach.push(convertBufferToScreenshotVariable(blob)); + } + } else if (pick.kind === 'command') { + // Dynamic variable with a followup command + const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); + if (!selection) { + // User made no selection, skip this variable + continue; + } + toAttach.push({ + ...pick, + value: pick.value, + name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, + // Apply the original icon with the new name + fullName: selection + }); + } else if (pick.kind === 'tool') { + toAttach.push({ + id: pick.id, + name: pick.tool.displayName, + fullName: pick.tool.displayName, + value: undefined, + icon: pick.icon, + kind: 'tool' + }); + } else if (pick.kind === 'image') { + const fileBuffer = await clipboardService.readImage(); + toAttach.push({ + id: await imageToHash(fileBuffer), + name: localize('pastedImage', 'Pasted Image'), + fullName: localize('pastedImage', 'Pasted Image'), + value: fileBuffer, + kind: 'image', + }); + } + } else if (isISymbolQuickPickItem(pick) && pick.symbol) { // Workspace symbol toAttach.push({ kind: 'symbol', @@ -507,24 +616,6 @@ export class AttachContextAction extends Action2 { fullName: pick.label, name: pick.symbol.name, }); - } else if (isIFolderSearchResultQuickPickItem(pick)) { - const folder = pick.resource; - toAttach.push({ - kind: 'directory', - id: pick.id, - value: folder, - name: basename(folder), - - }); - } else if (isIDiagnosticsQuickPickItemWithFilter(pick)) { - toAttach.push({ - id: pick.id, - name: pick.label, - value: pick.filter, - kind: 'diagnostic', - icon: pick.icon, - ...pick.filter, - }); } else if (isIQuickPickItemWithResource(pick) && pick.resource) { if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) { // checks if the file is an image @@ -565,107 +656,6 @@ export class AttachContextAction extends Action2 { fullName: pick.label, name: pick.symbolName!, }); - } else if (isIOpenEditorsQuickPickItem(pick)) { - for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { - const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; - if (uri) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: uri }), - value: uri, - name: labelService.getUriBasenameLabel(uri), - }); - } - } - } else if (isISearchResultsQuickPickItem(pick)) { - const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; - for (const result of searchView.model.searchResult.matches()) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: result.resource }), - value: result.resource, - name: labelService.getUriBasenameLabel(result.resource), - }); - } - } else if (isRelatedFileQuickPickItem(pick)) { - // Get all provider results and show them in a second tier picker - const chatSessionId = widget.viewModel?.sessionId; - if (!chatSessionId || !chatEditingService) { - continue; - } - const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); - if (!relatedFiles) { - continue; - } - const attachments = widget.attachmentModel.getAttachmentIDs(); - const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) - .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { - acc.push({ type: 'separator', label: cur.group }); - for (const file of cur.files) { - acc.push({ - type: 'item', - label: labelService.getUriBasenameLabel(file.uri), - description: labelService.getUriLabel(dirname(file.uri), { relative: true }), - value: file.uri, - disabled: attachments.has(this._getFileContextId({ resource: file.uri })), - picked: true - }); - } - return acc; - }, [])); - const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true }); - for (const file of selectedFiles ?? []) { - toAttach.push({ - kind: 'file', - id: this._getFileContextId({ resource: file.value }), - value: file.value, - name: file.label, - omittedState: OmittedState.NotOmitted - }); - } - } else if (isScreenshotQuickPickItem(pick)) { - const blob = await hostService.getScreenshot(); - if (blob) { - toAttach.push(convertBufferToScreenshotVariable(blob)); - } - } else if (isPromptInstructionsQuickPickItem(pick)) { - await runAttachPromptAction({ widget }, commandService); - } else { - // Anything else is an attachment - const attachmentPick = pick as IAttachmentQuickPickItem; - if (attachmentPick.kind === 'command') { - // Dynamic variable with a followup command - const selection = await commandService.executeCommand(attachmentPick.command.id, ...(attachmentPick.command.arguments ?? [])); - if (!selection) { - // User made no selection, skip this variable - continue; - } - toAttach.push({ - ...attachmentPick, - value: attachmentPick.value, - name: `${typeof attachmentPick.value === 'string' && attachmentPick.value.startsWith('#') ? attachmentPick.value.slice(1) : ''}${selection}`, - // Apply the original icon with the new name - fullName: selection - }); - } else if (attachmentPick.kind === 'tool') { - toAttach.push({ - id: attachmentPick.id, - name: attachmentPick.label, - fullName: attachmentPick.label, - value: undefined, - icon: attachmentPick.icon, - kind: 'tool' - }); - } else if (attachmentPick.kind === 'image') { - const fileBuffer = await clipboardService.readImage(); - toAttach.push({ - id: await imageToHash(fileBuffer), - name: localize('pastedImage', 'Pasted Image'), - fullName: localize('pastedImage', 'Pasted Image'), - value: fileBuffer, - kind: 'image', - }); - } } } @@ -678,30 +668,21 @@ export class AttachContextAction extends Action2 { } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const quickInputService = accessor.get(IQuickInputService); const chatAgentService = accessor.get(IChatAgentService); - const commandService = accessor.get(ICommandService); const widgetService = accessor.get(IChatWidgetService); - const languageModelToolsService = accessor.get(ILanguageModelToolsService); - const quickChatService = accessor.get(IQuickChatService); const clipboardService = accessor.get(IClipboardService); const editorService = accessor.get(IEditorService); - const labelService = accessor.get(ILabelService); const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const hostService = accessor.get(IHostService); const extensionService = accessor.get(IExtensionService); - const fileService = accessor.get(IFileService); - const textModelService = accessor.get(ITextModelService); const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); + const chatEditingService = accessor.get(IChatEditingService); - const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0]; + const context: { widget?: IChatWidget; placeholder?: string } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; if (!widget) { return; } - const chatEditingService = accessor.get(IChatEditingService); const quickPickItems: IAttachmentQuickPickItem[] = []; if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { @@ -749,27 +730,16 @@ export class AttachContextAction extends Action2 { } } - for (const tool of languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - const item: IToolQuickPickItem = { - kind: 'tool', - label: tool.displayName ?? '', - id: tool.id, - icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? - }; - if (ThemeIcon.isThemeIcon(tool.icon)) { - item.iconClass = ThemeIcon.asClassName(tool.icon); - } else if (tool.icon) { - item.iconPath = tool.icon; - } - - quickPickItems.push(item); - } - } + quickPickItems.push({ + kind: 'tools', + label: localize('chatContext.tools', 'Tools...'), + iconClass: ThemeIcon.asClassName(Codicon.tools), + id: 'tools', + }); quickPickItems.push({ kind: 'quickaccess', - label: localize('chatContext.symbol', 'Symbol...'), + label: localize('chatContext.symbol', 'Symbols...'), iconClass: ThemeIcon.asClassName(Codicon.symbolField), prefix: SymbolsQuickAccessProvider.PREFIX, id: 'symbol' @@ -777,14 +747,14 @@ export class AttachContextAction extends Action2 { quickPickItems.push({ kind: 'folder', - label: localize('chatContext.folder', 'Folder...'), + label: localize('chatContext.folder', 'Folders...'), iconClass: ThemeIcon.asClassName(Codicon.folder), id: 'folder', }); quickPickItems.push({ kind: 'diagnostic', - label: localize('chatContext.diagnstic', 'Problem...'), + label: localize('chatContext.diagnstic', 'Problems...'), iconClass: ThemeIcon.asClassName(Codicon.error), id: 'diagnostic' }); @@ -805,65 +775,54 @@ export class AttachContextAction extends Action2 { }); } - if (context?.showFilesOnly) { - if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || widget.attachmentModel.fileAttachments.length > 0)) { - quickPickItems.unshift({ - kind: 'related-files', - id: 'related-files', - label: localize('chatContext.relatedFiles', 'Related Files'), - iconClass: ThemeIcon.asClassName(Codicon.sparkle), - }); - } - if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { - quickPickItems.unshift({ - kind: 'open-editors', - id: 'open-editors', - label: localize('chatContext.editors', 'Open Editors'), - iconClass: ThemeIcon.asClassName(Codicon.files), - }); - } - if (SearchContext.HasSearchResults.getValue(contextKeyService)) { - quickPickItems.unshift({ - kind: 'search-results', - id: 'search-results', - label: localize('chatContext.searchResults', 'Search Results'), - iconClass: ThemeIcon.asClassName(Codicon.search), - }); - } + if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || widget.attachmentModel.fileAttachments.length > 0)) { + quickPickItems.push({ + kind: 'related-files', + id: 'related-files', + label: localize('chatContext.relatedFiles', 'Related Files'), + iconClass: ThemeIcon.asClassName(Codicon.sparkle), + }); + } + if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { + quickPickItems.push({ + kind: 'open-editors', + id: 'open-editors', + label: localize('chatContext.editors', 'Open Editors'), + iconClass: ThemeIcon.asClassName(Codicon.files), + }); + } + if (SearchContext.HasSearchResults.getValue(contextKeyService)) { + quickPickItems.push({ + kind: 'search-results', + id: 'search-results', + label: localize('chatContext.searchResults', 'Search Results'), + iconClass: ThemeIcon.asClassName(Codicon.search), + }); } // if the `reusable prompts` feature is enabled, add // the appropriate attachment type to the list if (widget.attachmentModel.promptInstructions.featureEnabled) { - const keybinding = keybindingService.lookupKeybinding(USE_PROMPT_COMMAND_ID, contextKeyService); + const keybinding = keybindingService.lookupKeybinding(INSTRUCTIONS_COMMAND_ID, contextKeyService); quickPickItems.push({ - id: REUSABLE_PROMPT_PICK_ID, - kind: REUSABLE_PROMPT_PICK_ID, - label: localize('chatContext.attach.prompt.label', 'Prompt...'), + id: INSTRUCTION_PICK_ID, + kind: INSTRUCTION_PICK_ID, + label: localize('chatContext.attach.instructions.label', 'Instructions...'), iconClass: ThemeIcon.asClassName(Codicon.bookmark), keybinding, }); } - function extractTextFromIconLabel(label: string | undefined): string { - if (!label) { - return ''; + quickPickItems.sort((a, b) => { + let result = attachmentsOrdinals.indexOf(b.kind) - attachmentsOrdinals.indexOf(a.kind); + if (result === 0) { + result = a.label.localeCompare(b.label); } - const match = label.match(/\$\([^\)]+\)\s*(.+)/); - return match ? match[1] : label; - } + return result; + }); - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems.sort(function (a, b) { - - if (a.kind === 'open-editors') { return -1; } - if (b.kind === 'open-editors') { return 1; } - - const first = extractTextFromIconLabel(a.label).toUpperCase(); - const second = extractTextFromIconLabel(b.label).toUpperCase(); - - return compare(first, second); - }), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', context?.placeholder); + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', context?.placeholder); } private async _showDiagnosticsPick(instantiationService: IInstantiationService, onBackgroundAccept: (item: IChatContextQuickPickItem[]) => void): Promise { @@ -875,48 +834,61 @@ export class AttachContextAction extends Action2 { filter: item, }); - const filter = await instantiationService.invokeFunction(accessor => - createMarkersQuickPick(accessor, 'problem', items => onBackgroundAccept(items.map(convert)))); + const filter = await instantiationService.invokeFunction(createMarkersQuickPick, items => onBackgroundAccept(items.map(convert))); return filter && convert(filter); } - private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, instantiationService: IInstantiationService, query: string = '', placeholder?: string) { + private _show(accessor: ServicesAccessor, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, query: string = '', placeholder?: string) { + const quickInputService = accessor.get(IQuickInputService); + const quickChatService = accessor.get(IQuickChatService); + const editorService = accessor.get(IEditorService); + const commandService = accessor.get(ICommandService); + const instantiationService = accessor.get(IInstantiationService); + const attach = (isBackgroundAccept: boolean, ...items: IChatContextQuickPickItem[]) => { - this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, isBackgroundAccept, ...items); + instantiationService.invokeFunction(this._attachContext.bind(this), widget, isBackgroundAccept, ...items); }; const providerOptions: AnythingQuickAccessProviderRunOptions = { + additionPicks: quickPickItems, handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => { let item: IChatContextQuickPickItem | undefined = inputItem; - if ('kind' in item && item.kind === 'folder') { - item = await this._showFolders(instantiationService); - } else if ('kind' in item && item.kind === 'diagnostic') { - item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i)); - } - if (!item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder); - return; - } + if (isIAttachmentQuickPickItem(item)) { - if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, item.prefix, placeholder); - } else { - if (!clipboardService) { + if (item.kind === 'quickaccess') { + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, item.prefix, placeholder); + return; + } else if (item.kind === 'instructions') { + runAttachInstructionsAction(commandService, { widget }); return; } - attach(isBackgroundAccept, item); - if (isQuickChat(widget)) { - quickChatService.open(); + + if (item.kind === 'folder') { + item = await this._showFolders(instantiationService); + } else if (item.kind === 'diagnostic') { + item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i)); + } else if (item.kind === 'tools') { + item = await instantiationService.invokeFunction(showToolsPick, widget); } + if (!item) { + // restart picker when sub-picker didn't return anything + instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', placeholder); + return; + } + } + attach(isBackgroundAccept, item); + if (isQuickChat(widget)) { + quickChatService.open(); + } + }, - additionPicks: quickPickItems, filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); - if (isIOpenEditorsQuickPickItem(item)) { + if (isIAttachmentQuickPickItem(item) && item.kind === 'open-editors') { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) { // There is an open editor that hasn't yet been attached to the chat if (editor.resource && !attachedContext.has(this._getFileContextId({ resource: editor.resource }))) { @@ -977,7 +949,125 @@ export class AttachContextAction extends Action2 { } } +async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { + const quickInputService = accessor.get(IQuickInputService); + const markerService = accessor.get(IMarkerService); + const labelService = accessor.get(ILabelService); + + const markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); + const grouped = groupBy(markers, (a, b) => extUri.compare(a.resource, b.resource)); + + const severities = new Set(); + type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; + const items: (MarkerPickItem | IQuickPickSeparator)[] = []; + + let pickCount = 0; + for (const group of grouped) { + const resource = group[0].resource; + + items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); + for (const marker of group) { + pickCount++; + severities.add(marker.severity); + items.push({ + type: 'item', + resource: marker.resource, + label: marker.message, + description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), + entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), + }); + } + } + + items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); + + const store = new DisposableStore(); + const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); + quickPick.canAcceptInBackground = !onBackgroundAccept; + quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); + quickPick.items = items; + + return new Promise(resolve => { + store.add(quickPick.onDidHide(() => resolve(undefined))); + store.add(quickPick.onDidAccept(ev => { + if (ev.inBackground) { + onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); + } else { + resolve(quickPick.selectedItems[0]?.entry); + quickPick.dispose(); + } + })); + quickPick.show(); + }).finally(() => store.dispose()); +} + +async function showToolsPick(accessor: ServicesAccessor, widget: IChatWidget): Promise { + + const quickPickService = accessor.get(IQuickInputService); + + + function classify(tool: IToolData) { + if (tool.source.type === 'internal' || tool.source.type === 'extension' && !tool.source.isExternalTool) { + return { ordinal: 1, groupLabel: localize('chatContext.tools.internal', 'Built-In') }; + } else if (tool.source.type === 'mcp') { + return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') }; + } else { + return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') }; + } + } + + type Pick = IToolQuickPickItem & { ordinal: number; groupLabel: string }; + const items: Pick[] = []; + + for (const tool of widget.input.selectedToolsModel.tools.get()) { + if (!tool.canBeReferencedInPrompt) { + continue; + } + const item: Pick = { + tool, + ...classify(tool), + kind: 'tool', + label: tool.toolReferenceName ?? tool.id, + description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined, + id: tool.id, + }; + // if (ThemeIcon.isThemeIcon(tool.icon)) { + // item.iconClass = ThemeIcon.asClassName(tool.icon); + // } else if (tool.icon) { + // item.iconPath = tool.icon; + // } + items.push(item); + } + + items.sort((a, b) => { + let res = a.ordinal - b.ordinal; + if (res === 0) { + res = a.label.localeCompare(b.label); + } + return res; + }); + + let lastGroupLabel: string | undefined; + const picks: (IQuickPickSeparator | Pick)[] = []; + + + for (const item of items) { + if (lastGroupLabel !== item.groupLabel) { + picks.push({ type: 'separator', label: item.groupLabel }); + lastGroupLabel = item.groupLabel; + } + picks.push(item); + } + + const result = await quickPickService.pick(picks, { + placeHolder: localize('chatContext.tools.placeholder', 'Select a tool'), + canPickMany: false + }); + + return result; +} + /** * Register all actions related to reusable prompt files. */ -registerReusablePromptActions(); +registerPromptActions(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 1c09419c4c6..f19d7f56cc3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -22,7 +22,7 @@ export function registerChatCopyActions() { category: CHAT_CATEGORY, menu: { id: MenuId.ChatContext, - when: ChatContextKeys.responseIsFiltered.toNegated(), + when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', } }); @@ -54,7 +54,7 @@ export function registerChatCopyActions() { category: CHAT_CATEGORY, menu: { id: MenuId.ChatContext, - when: ChatContextKeys.responseIsFiltered.toNegated(), + when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', } }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 356c6cea820..459203230b4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -51,13 +51,7 @@ export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; constructor() { - const precondition = ContextKeyExpr.and( - // if the input has prompt instructions attached, allow submitting requests even - // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - whenNotInProgressOrPaused, - ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), - ); + const precondition = ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask); super({ id: ChatSubmitAction.ID, @@ -76,14 +70,14 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecuteSecondary, group: 'group_1', order: 1, - when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask) + when: precondition }, { id: MenuId.ChatExecute, order: 4, when: ContextKeyExpr.and( whenNotInProgressOrPaused, - ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), + precondition, ), group: 'navigation', }, @@ -170,7 +164,7 @@ class ToggleChatModeAction extends Action2 { } else { const confirmation = await dialogService.confirm({ title: localize('agent.newSession', "Start new session?"), - message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to continue?"), + message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to change the chat mode?"), primaryButton: localize('agent.newSession.confirm', "Yes"), type: 'info' }); @@ -240,9 +234,8 @@ export class ToggleRequestPausedAction extends Action2 { } } -export const ChatSwitchToNextModelActionId = 'workbench.action.chat.switchToNextModel'; -export class SwitchToNextModelAction extends Action2 { - static readonly ID = ChatSwitchToNextModelActionId; +class SwitchToNextModelAction extends Action2 { + static readonly ID = 'workbench.action.chat.switchToNextModel'; constructor() { super({ @@ -250,6 +243,27 @@ export class SwitchToNextModelAction extends Action2 { title: localize2('interactive.switchToNextModel.label', "Switch to Next Model"), category: CHAT_CATEGORY, f1: true, + precondition: ChatContextKeys.enabled, + }); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + widget?.input.switchToNextModel(); + } +} + +export const ChatOpenModelPickerActionId = 'workbench.action.chat.openModelPicker'; +class OpenModelPickerAction extends Action2 { + static readonly ID = ChatOpenModelPickerActionId; + + constructor() { + super({ + id: OpenModelPickerAction.ID, + title: localize2('interactive.openModelPicker.label', "Open Model Picker"), + category: CHAT_CATEGORY, + f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Period, weight: KeybindingWeight.WorkbenchContrib, @@ -276,7 +290,9 @@ export class SwitchToNextModelAction extends Action2 { override run(accessor: ServicesAccessor, ...args: any[]): void { const widgetService = accessor.get(IChatWidgetService); const widget = widgetService.lastFocusedWidget; - widget?.input.switchToNextModel(); + if (widget) { + widget.input.openModelPicker(); + } } } @@ -284,13 +300,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.edits.submit'; constructor() { - const precondition = ContextKeyExpr.and( - // if the input has prompt instructions attached, allow submitting requests even - // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), - whenNotInProgressOrPaused, - ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask), - ); + const precondition = ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask); super({ id: ChatEditingSessionSubmitAction.ID, @@ -308,7 +318,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { { id: MenuId.ChatExecuteSecondary, group: 'group_1', - when: ContextKeyExpr.and(whenNotInProgressOrPaused, ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask),), + when: ContextKeyExpr.and(whenNotInProgressOrPaused, precondition), order: 1 }, { @@ -319,7 +329,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { ContextKeyExpr.and(ChatContextKeys.isRequestPaused, ChatContextKeys.inputHasText), ChatContextKeys.requestInProgress.negate(), ), - ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask),), + precondition), group: 'navigation', }, ] @@ -334,7 +344,7 @@ class SubmitWithoutDispatchingAction extends Action2 { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ); @@ -377,7 +387,7 @@ export class ChatSubmitWithCodebaseAction extends Action2 { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ); @@ -431,7 +441,7 @@ class SendToNewChatAction extends Action2 { const precondition = ContextKeyExpr.and( // if the input has prompt instructions attached, allow submitting requests even // without text present - having instructions is enough context for a request - ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.instructionsAttached), + ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile), whenNotInProgressOrPaused, ); @@ -523,4 +533,5 @@ export function registerChatExecuteActions() { registerAction2(ToggleChatModeAction); registerAction2(ToggleRequestPausedAction); registerAction2(SwitchToNextModelAction); + registerAction2(OpenModelPickerAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index ce4d1e6a6b6..b70f7e04a70 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -20,7 +20,6 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { AddConfigurationAction } from '../../../mcp/browser/mcpCommands.js'; import { IMcpService, IMcpServer, McpConnectionState } from '../../../mcp/common/mcpTypes.js'; @@ -111,7 +110,6 @@ export class AttachToolsAction extends Action2 { const quickPickService = accessor.get(IQuickInputService); const mcpService = accessor.get(IMcpService); const toolsService = accessor.get(ILanguageModelToolsService); - const extensionService = accessor.get(IExtensionService); const chatWidgetService = accessor.get(IChatWidgetService); const telemetryService = accessor.get(ITelemetryService); const commandService = accessor.get(ICommandService); @@ -185,7 +183,8 @@ export class AttachToolsAction extends Action2 { if (!mcpServer) { continue; } - bucket = toolBuckets.get(mcpServer.definition.id) ?? { + const key = tool.source.type + mcpServer.definition.id; + bucket = toolBuckets.get(key) ?? { type: 'item', label: localize('mcplabel', "MCP Server: {0}", mcpServer?.definition.label), status: localize('mcpstatus', "From {0} ({1})", mcpServer.collection.label, McpConnectionState.toString(mcpServer.connectionState.get())), @@ -194,23 +193,19 @@ export class AttachToolsAction extends Action2 { picked: false, children: [] }; - toolBuckets.set(mcpServer.definition.id, bucket); + toolBuckets.set(key, bucket); } else if (tool.source.type === 'extension') { - const extensionId = tool.source.extensionId; - const ext = extensionService.extensions.find(value => ExtensionIdentifier.equals(value.identifier, extensionId)); - if (!ext) { - continue; - } + const key = tool.source.type + ExtensionIdentifier.toKey(tool.source.extensionId); - bucket = toolBuckets.get(ExtensionIdentifier.toKey(extensionId)) ?? { + bucket = toolBuckets.get(key) ?? { type: 'item', - label: ext.displayName ?? ext.name, + label: tool.source.label, ordinal: BucketOrdinal.Extension, picked: false, source: tool.source, children: [] }; - toolBuckets.set(ExtensionIdentifier.toKey(ext.identifier), bucket); + toolBuckets.set(key, bucket); } else if (tool.source.type === 'internal') { bucket = defaultBucket; } else { diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index f6d1050cbd5..6858766505a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -35,6 +35,7 @@ import { ICodeBlockActionContext } from '../codeBlockPart.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; export class InsertCodeBlockOperation { constructor( @@ -117,6 +118,7 @@ export class ApplyCodeBlockOperation { @IQuickInputService private readonly quickInputService: IQuickInputService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotebookService private readonly notebookService: INotebookService, ) { } @@ -128,7 +130,7 @@ export class ApplyCodeBlockOperation { return; } - if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri)) { + if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri) && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { // reveal the target file try { const editorPane = await this.editorService.openEditor({ resource: codemapperUri }); @@ -148,7 +150,7 @@ export class ApplyCodeBlockOperation { let result: IComputeEditsResult | undefined = undefined; - if (activeEditorControl) { + if (activeEditorControl && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { result = await this.handleTextEditor(activeEditorControl, context.code); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts new file mode 100644 index 00000000000..1df01e8afcd --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../chat.js'; +import { CHAT_CATEGORY } from '../chatActions.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize, localize2 } from '../../../../../../nls.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; +import { PromptFilePickers } from './dialogs/askToSelectPrompt/promptFilePickers.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { attachInstructionsFiles, IAttachOptions } from './dialogs/askToSelectPrompt/utils/attachInstructions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * Action ID for the `Attach Instruction` action. + */ +const ATTACH_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.attach.instructions'; + +/** + * Options for the {@link AttachInstructionsAction} action. + */ +export interface IAttachInstructionsActionOptions { + + /** + * Target chat widget reference to attach the instruction to. If the reference is + * provided, the command will attach the instruction as attachment of the widget. + * Otherwise, the command will re-use an existing one. + */ + readonly widget?: IChatWidget; + + /** + * Instruction resource `URI` to attach to the chat input, if any. + * If provided the resource will be pre-selected in the prompt picker dialog, + * otherwise the dialog will show the prompts list without any pre-selection. + */ + readonly resource?: URI; + + /** + * Whether to skip the instructions files selection dialog. + * + * Note! if this option is set to `true`, the {@link resource} + * option `must be defined`. + */ + readonly skipSelectionDialog?: boolean; +} + +/** + * Action to attach a prompt to a chat widget input. + */ +class AttachInstructionsAction extends Action2 { + constructor() { + super({ + id: ATTACH_INSTRUCTIONS_ACTION_ID, + title: localize2('attach-instructions.capitalized.ellipses', "Attach Instructions..."), + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + }); + } + + public override async run( + accessor: ServicesAccessor, + options: IAttachInstructionsActionOptions, + ): Promise { + const viewsService = accessor.get(IViewsService); + const promptsService = accessor.get(IPromptsService); + const commandService = accessor.get(ICommandService); + const instaService = accessor.get(IInstantiationService); + + const pickers = instaService.createInstance(PromptFilePickers); + + const { skipSelectionDialog, resource } = options; + + const attachOptions: IAttachOptions = { + widget: options.widget, + viewsService, + commandService, + }; + + if (skipSelectionDialog === true) { + assertDefined( + resource, + 'Resource must be defined when skipping prompt selection dialog.', + ); + + const { widget } = await attachInstructionsFiles( + [resource], + attachOptions, + ); + + widget.focusInput(); + + return; + } + + // find all prompt files in the user workspace + const promptFiles = await promptsService.listPromptFiles('instructions'); + const placeholder = localize( + 'commands.instructions.select-dialog.placeholder', + 'Select instructions files to attach', + ); + + const instructions = await pickers.selectInstructionsFiles({ promptFiles, placeholder }); + + if (instructions !== undefined) { + const { widget } = await attachInstructionsFiles( + instructions, + attachOptions, + ); + widget.focusInput(); + } + } +} + +/** + * Runs the `Attach Instructions` action with provided options. We export this + * function instead of {@link ATTACH_INSTRUCTIONS_ACTION_ID} directly to + * encapsulate/enforce the correct options to be passed to the action. + */ +export const runAttachInstructionsAction = async ( + commandService: ICommandService, + options: IAttachInstructionsActionOptions, +): Promise => { + return await commandService.executeCommand( + ATTACH_INSTRUCTIONS_ACTION_ID, + options, + ); +}; + +/** + * Helper to register the `Attach Prompt` action. + */ +export const registerAttachPromptActions = () => { + registerAction2(AttachInstructionsAction); +}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts similarity index 66% rename from src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts rename to src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts index 7c7ae5472a4..6221f2cd06a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatRunPromptAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatRunPromptAction.ts @@ -6,26 +6,30 @@ import { IChatWidget } from '../../chat.js'; import { CHAT_CATEGORY } from '../chatActions.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { OS } from '../../../../../../base/common/platform.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { ChatContextKeys } from '../../../common/chatContextKeys.js'; import { assertDefined } from '../../../../../../base/common/types.js'; -import { ILocalizedString, localize2 } from '../../../../../../nls.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { ResourceContextKey } from '../../../../../common/contextkeys.js'; import { KeyCode, KeyMod } from '../../../../../../base/common/keyCodes.js'; import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; -import { attachPrompt } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; -import { detachPrompt } from './dialogs/askToSelectPrompt/utils/detachPrompt.js'; -import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; +import { ILocalizedString, localize, localize2 } from '../../../../../../nls.js'; +import { UILabelProvider } from '../../../../../../base/common/keybindingLabels.js'; import { ICommandAction } from '../../../../../../platform/action/common/action.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; import { IViewsService } from '../../../../../services/views/common/viewsService.js'; +import { PromptFilePickers } from './dialogs/askToSelectPrompt/promptFilePickers.js'; import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; import { EditorContextKeys } from '../../../../../../editor/common/editorContextKeys.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { getActivePromptUri } from '../../promptSyntax/contributions/usePromptCommand.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IRunPromptOptions, runPromptFile } from './dialogs/askToSelectPrompt/utils/runPrompt.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; /** * Condition for the `Run Current Prompt` action. @@ -46,6 +50,11 @@ const COMMAND_KEY_BINDING = KeyMod.WinCtrl | KeyCode.Slash | KeyMod.Alt; */ const RUN_CURRENT_PROMPT_ACTION_ID = 'workbench.action.chat.run.prompt.current'; +/** + * Action ID for the `Run Prompt...` action. + */ +const RUN_SELECTED_PROMPT_ACTION_ID = 'workbench.action.chat.run.prompt'; + /** * Constructor options for the `Run Prompt` base action. */ @@ -121,31 +130,21 @@ abstract class RunPromptBaseAction extends Action2 { const viewsService = accessor.get(IViewsService); const commandService = accessor.get(ICommandService); - resource ||= getActivePromptUri(accessor); + resource ||= getActivePromptFileUri(accessor); assertDefined( resource, 'Cannot find URI resource for an active text editor.', ); - const { widget, wasAlreadyAttached } = await attachPrompt( + const { widget } = await runPromptFile( resource, { inNewChat, - skipIfImplicitlyAttached: true, commandService, viewsService, }, ); - // submit the prompt immediately - await widget.acceptInput(); - - // detach the prompt immediately, unless was attached - // before the action was executed - if (wasAlreadyAttached === false) { - await detachPrompt(resource, { widget }); - } - return widget; } } @@ -182,6 +181,77 @@ class RunCurrentPromptAction extends RunPromptBaseAction { } } +class RunSelectedPromptAction extends Action2 { + constructor() { + super({ + id: RUN_SELECTED_PROMPT_ACTION_ID, + title: localize2('run-prompt.capitalized.ellipses', "Run Prompt..."), + icon: Codicon.bookmark, + f1: true, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + keybinding: { + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + weight: KeybindingWeight.WorkbenchContrib, + primary: COMMAND_KEY_BINDING, + }, + category: CHAT_CATEGORY, + }); + } + + public override async run( + accessor: ServicesAccessor, + ): Promise { + const viewsService = accessor.get(IViewsService); + const promptsService = accessor.get(IPromptsService); + const commandService = accessor.get(ICommandService); + const instaService = accessor.get(IInstantiationService); + + const pickers = instaService.createInstance(PromptFilePickers); + + // find all prompt files in the user workspace + const promptFiles = await promptsService.listPromptFiles('prompt'); + const placeholder = localize( + 'commands.prompt.select-dialog.placeholder', + 'Select the prompt file to run (hold {0}-key to use in new chat)', + UILabelProvider.modifierLabels[OS].ctrlKey + ); + + const result = await pickers.selectPromptFile({ promptFiles, placeholder }); + + if (result === undefined) { + return; + } + + const { promptFile, keyMods } = result; + const runPromptOptions: IRunPromptOptions = { + inNewChat: keyMods.ctrlCmd, + viewsService, + commandService, + }; + const { widget } = await runPromptFile( + promptFile, + runPromptOptions, + ); + widget.focusInput(); + } +} + + +/** + * Gets `URI` of a prompt file open in an active editor instance, if any. + */ +export const getActivePromptFileUri = ( + accessor: ServicesAccessor, +): URI | undefined => { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === PROMPT_LANGUAGE_ID) { + return model.uri; + } + return undefined; +}; + + /** * Action ID for the `Run Current Prompt In New Chat` action. */ @@ -228,4 +298,5 @@ class RunCurrentPromptInNewChatAction extends RunPromptBaseAction { export const registerRunPromptActions = () => { registerAction2(RunCurrentPromptAction); registerAction2(RunCurrentPromptInNewChatAction); + registerAction2(RunSelectedPromptAction); }; diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts new file mode 100644 index 00000000000..12bdc4132e5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatSaveToPromptAction.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../chat.js'; +import { CHAT_CATEGORY } from '../chatActions.js'; +import { localize2 } from '../../../../../../nls.js'; +import { IEditorPane } from '../../../../../common/editor.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { chatSubcommandLeader, IParsedChatRequest } from '../../../common/chatParserTypes.js'; +import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; + +/** + * Action ID for the `Save Prompt` action. + */ +const SAVE_TO_PROMPT_ACTION_ID = 'workbench.action.chat.save-to-prompt'; + +/** + * Name of the in-chat slash command associated with this action. + */ +export const SAVE_TO_PROMPT_SLASH_COMMAND_NAME = 'save'; + +/** + * Options for the {@link SaveToPromptAction} action. + */ +interface ISaveToPromptActionOptions { + /** + * Chat widget reference to save session of. + */ + chat: IChatWidget; +} + +/** + * Class that defines the `Save Prompt` action. + */ +class SaveToPromptAction extends Action2 { + constructor() { + super({ + id: SAVE_TO_PROMPT_ACTION_ID, + title: localize2( + 'workbench.actions.save-to-prompt.label', + "Save chat session to a prompt file", + ), + f1: false, + precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + category: CHAT_CATEGORY, + }); + } + + public async run( + accessor: ServicesAccessor, + options: ISaveToPromptActionOptions, + ): Promise { + const logService = accessor.get(ILogService); + const editorService = accessor.get(IEditorService); + + const logPrefix = 'save to prompt'; + const { chat } = options; + + const { viewModel } = chat; + assertDefined( + viewModel, + 'No view model found on currently the active chat widget.', + ); + + const { model } = viewModel; + + const turns: ITurn[] = []; + for (const request of model.getRequests()) { + const { message, response: responseModel } = request; + + if (isSaveToPromptSlashCommand(message)) { + continue; + } + + if (responseModel === undefined) { + logService.warn( + `[${logPrefix}]: skipping request '${request.id}' with no response`, + ); + + continue; + } + + const { response } = responseModel; + + const tools = new Set(); + for (const record of response.value) { + if (('toolId' in record) === false) { + continue; + } + + tools.add(record.toolId); + } + + turns.push({ + request: message.text, + response: response.getMarkdown(), + tools, + }); + } + + const promptText = renderPrompt(turns); + + const editor = await editorService.openEditor({ + resource: undefined, + contents: promptText, + languageId: PROMPT_LANGUAGE_ID, + }); + + assertDefined( + editor, + 'Failed to open untitled editor for the prompt.', + ); + + editor.focus(); + + return editor; + } +} + +/** + * Check if provided message belongs to the `save to prompt` slash + * command itself that was run in the chat to invoke this action. + */ +const isSaveToPromptSlashCommand = ( + message: IParsedChatRequest, +): boolean => { + const { parts } = message; + if (parts.length < 1) { + return false; + } + + const firstPart = parts[0]; + if (firstPart.kind !== 'slash') { + return false; + } + + if (firstPart.text !== `${chatSubcommandLeader}${SAVE_TO_PROMPT_SLASH_COMMAND_NAME}`) { + return false; + } + + return true; +}; + +/** + * Render the response part of a `request`/`response` turn pair. + */ +const renderResponse = ( + response: string, +): string => { + // if response starts with a code block, add an extra new line + // before it, to prevent full blockquote from being be broken + const delimiter = (response.startsWith('```')) + ? '\n>' + : ' '; + + // add `>` to the beginning of each line of the response + // so it looks like a blockquote citing Copilot + const quotedResponse = response.replaceAll('\n', '\n> '); + + return `> Copilot:${delimiter}${quotedResponse}`; +}; + +/** + * Render a single `request`/`response` turn of the chat session. + */ +const renderTurn = ( + turn: ITurn, +): string => { + const { request, response } = turn; + + return `\n${request}\n\n${renderResponse(response)}`; +}; + +/** + * Render the entire chat session as a markdown prompt. + */ +const renderPrompt = ( + turns: readonly ITurn[], +): string => { + const content: string[] = []; + const allTools = new Set(); + + // render each turn and collect tool names + // that were used in the each turn + for (const turn of turns) { + content.push(renderTurn(turn)); + + // collect all used tools into a set of strings + for (const tool of turn.tools) { + allTools.add(tool); + } + } + + const result = []; + + // add prompt header + if (allTools.size !== 0) { + result.push(renderHeader(allTools)); + } + + // add chat request/response turns + result.push( + content.join('\n'), + ); + + // add trailing empty line + result.push(''); + + return result.join('\n'); +}; + + +/** + * Render the `tools` metadata inside prompt header. + */ +const renderTools = ( + tools: Set, +): string => { + const toolStrings = [...tools].map((tool) => { + return `'${tool}'`; + }); + + return `tools: [${toolStrings.join(', ')}]`; +}; + +/** + * Render prompt header. + */ +const renderHeader = ( + tools: Set, +): string => { + // skip rendering the header if no tools provided + if (tools.size === 0) { + return ''; + } + + return [ + '---', + renderTools(tools), + '---', + ].join('\n'); +}; + +/** + * Interface for a single `request`/`response` turn + * of a chat session. + */ +interface ITurn { + request: string; + response: string; + tools: Set; +} + +/** + * Runs the `Save To Prompt` action with provided options. We export this + * function instead of {@link SAVE_TO_PROMPT_ACTION_ID} directly to + * encapsulate/enforce the correct options to be passed to the action. + */ +export const runSaveToPromptAction = async ( + options: ISaveToPromptActionOptions, + commandService: ICommandService, +) => { + return await commandService.executeCommand( + SAVE_TO_PROMPT_ACTION_ID, + options, + ); +}; + +/** + * Helper to register all the `Save Prompt` actions. + */ +export const registerSaveToPromptActions = () => { + registerAction2(SaveToPromptAction); +}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts new file mode 100644 index 00000000000..5bc0426e887 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts @@ -0,0 +1,434 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../../../nls.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { OS } from '../../../../../../../../base/common/platform.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { Codicon } from '../../../../../../../../base/common/codicons.js'; +import { WithUriValue } from '../../../../../../../../base/common/types.js'; +import { ThemeIcon } from '../../../../../../../../base/common/themables.js'; +import { IPromptPath } from '../../../../../common/promptSyntax/service/types.js'; +import { dirname, extUri } from '../../../../../../../../base/common/resources.js'; +import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; +import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; +import { UILabelProvider } from '../../../../../../../../base/common/keybindingLabels.js'; +import { IDialogService } from '../../../../../../../../platform/dialogs/common/dialogs.js'; +import { getCleanPromptName } from '../../../../../../../../platform/prompts/common/constants.js'; +import { INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; +import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent } from '../../../../../../../../platform/quickinput/common/quickInput.js'; +import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; +import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID } from '../../../../promptSyntax/contributions/createPromptCommand/createPromptCommand.js'; + +/** + * Options for the {@link askToSelectInstructions} function. + */ +export interface ISelectOptions { + + /** + * The text shows as placeholder in the selection dialog. + */ + readonly placeholder: string; + + /** + * Prompt resource `URI` to attach to the chat input, if any. + * If provided the resource will be pre-selected in the prompt picker dialog, + * otherwise the dialog will show the prompts list without any pre-selection. + */ + readonly resource?: URI; + + /** + * List of prompt files to show in the selection dialog. + */ + readonly promptFiles: readonly IPromptPath[]; +} + +export interface ISelectPromptResult { + /** + * The selected prompt file. + */ + readonly promptFile: URI; + + /** + * The key modifiers that were pressed when the prompt was selected. + */ + readonly keyMods: IKeyMods; +} + +/** + * Button that opems the documentation. + */ +const HELP_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize('help', "help"), + iconClass: ThemeIcon.asClassName(Codicon.question), +}); + +/** + * A quick pick item that starts the 'New Prompt File' command. + */ +const NEW_PROMPT_FILE_OPTION: WithUriValue = Object.freeze({ + type: 'item', + label: `$(plus) ${localize( + 'commands.new-promptfile.select-dialog.label', + 'New prompt file...' + )}`, + value: URI.parse(PROMPT_DOCUMENTATION_URL), + pickable: false, + buttons: [HELP_BUTTON], +}); + +/** + * A quick pick item that starts the 'New Instructions File' command. + */ +const NEW_INSTRUCTIONS_FILE_OPTION: WithUriValue = Object.freeze({ + type: 'item', + label: `$(plus) ${localize( + 'commands.new-instructionsfile.select-dialog.label', + 'New instructions file...', + )}`, + value: URI.parse(INSTRUCTIONS_DOCUMENTATION_URL), + pickable: false, + buttons: [HELP_BUTTON], +}); + + +/** + * Button that opens a prompt file in the editor. + */ +const EDIT_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize( + 'commands.prompts.use.select-dialog.open-button.tooltip', + "edit ({0}-key + enter)", + UILabelProvider.modifierLabels[OS].ctrlKey + ), + iconClass: ThemeIcon.asClassName(Codicon.edit), +}); + +/** + * Button that deletes a prompt file. + */ +const DELETE_BUTTON: IQuickInputButton = Object.freeze({ + tooltip: localize('delete', "delete"), + iconClass: ThemeIcon.asClassName(Codicon.trash), +}); + + +export class PromptFilePickers { + constructor( + @ILabelService private readonly _labelService: ILabelService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IOpenerService private readonly _openerService: IOpenerService, + @IFileService private readonly _fileService: IFileService, + @IDialogService private readonly _dialogService: IDialogService, + @ICommandService private readonly _commandService: ICommandService, + ) { + } + /** + * Shows the instructions selection dialog to the user that allows to select a instructions file(s). + * + * If {@link ISelectOptions.resource resource} is provided, the dialog will have + * the resource pre-selected in the prompts list. + */ + public async selectInstructionsFiles(options: ISelectOptions): Promise { + + const fileOptions = this._createPromptPickItems(options); + fileOptions.splice(0, 0, NEW_INSTRUCTIONS_FILE_OPTION); + + const quickPick = this._quickInputService.createQuickPick>(); + quickPick.activeItems = fileOptions.length ? [fileOptions[0]] : []; + quickPick.placeholder = options.placeholder; + quickPick.canAcceptInBackground = true; + quickPick.matchOnDescription = true; + quickPick.items = fileOptions; + quickPick.canSelectMany = true; + + return new Promise(resolve => { + const disposables = new DisposableStore(); + + let isResolved = false; + + // then the dialog is hidden or disposed for other reason, + // dispose everything and resolve the main promise + disposables.add({ + dispose() { + quickPick.dispose(); + if (!isResolved) { + resolve(undefined); + isResolved = true; + } + }, + }); + + // handle the prompt `accept` event + disposables.add(quickPick.onDidAccept(async (event) => { + const { selectedItems } = quickPick; + + if (selectedItems[0] === NEW_INSTRUCTIONS_FILE_OPTION) { + await this._commandService.executeCommand(NEW_INSTRUCTIONS_COMMAND_ID); + return; + } + + resolve(selectedItems.map(item => item.value)); + isResolved = true; + + // if user submitted their selection, close the dialog + if (!event.inBackground) { + disposables.dispose(); + } + })); + + // handle the `button click` event on a list item (edit, delete, etc.) + disposables.add(quickPick.onDidTriggerItemButton( + e => this._handleButtonClick(quickPick, e)) + ); + + // when the dialog is hidden, dispose everything + disposables.add(quickPick.onDidHide( + disposables.dispose.bind(disposables), + )); + + // finally, reveal the dialog + quickPick.show(); + }); + } + + /** + * Shows the instructions selection dialog to the user that allows to select a instructions file(s). + * + * If {@link ISelectOptions.resource resource} is provided, the dialog will have + * the resource pre-selected in the prompts list. + */ + public async selectPromptFile(options: ISelectOptions): Promise { + + const fileOptions = this._createPromptPickItems(options); + fileOptions.splice(0, 0, NEW_PROMPT_FILE_OPTION); + + const quickPick = this._quickInputService.createQuickPick>(); + quickPick.activeItems = fileOptions.length ? [fileOptions[0]] : []; + quickPick.placeholder = options.placeholder; + quickPick.canAcceptInBackground = true; + quickPick.matchOnDescription = true; + quickPick.items = fileOptions; + + return new Promise(resolve => { + const disposables = new DisposableStore(); + + let isResolved = false; + + // then the dialog is hidden or disposed for other reason, + // dispose everything and resolve the main promise + disposables.add({ + dispose() { + quickPick.dispose(); + if (!isResolved) { + resolve(undefined); + isResolved = true; + } + }, + }); + + // handle the prompt `accept` event + disposables.add(quickPick.onDidAccept(async (event) => { + const { selectedItems } = quickPick; + const { keyMods } = quickPick; + + const selectedItem = selectedItems[0]; + if (selectedItem === NEW_PROMPT_FILE_OPTION) { + await this._commandService.executeCommand(NEW_PROMPT_COMMAND_ID); + return; + } + + if (selectedItem) { + resolve({ promptFile: selectedItem.value, keyMods: { ...keyMods } }); + isResolved = true; + } + + // if user submitted their selection, close the dialog + if (!event.inBackground) { + disposables.dispose(); + } + })); + + // handle the `button click` event on a list item (edit, delete, etc.) + disposables.add(quickPick.onDidTriggerItemButton( + e => this._handleButtonClick(quickPick, e)) + ); + + // when the dialog is hidden, dispose everything + disposables.add(quickPick.onDidHide( + disposables.dispose.bind(disposables), + )); + + // finally, reveal the dialog + quickPick.show(); + }); + } + + private _createPromptPickItems(options: ISelectOptions): WithUriValue[] { + const { promptFiles, resource } = options; + + const fileOptions = promptFiles.map((promptFile) => { + return this._createPromptPickItem(promptFile); + }); + + // if a resource is provided, create an `activeItem` for it to pre-select + // it in the UI, and sort the list so the active item appears at the top + let activeItem: WithUriValue | undefined; + if (resource) { + activeItem = fileOptions.find((file) => { + return extUri.isEqual(file.value, resource); + }); + + // if no item for the `resource` was found, it means that the resource is not + // in the list of prompt files, so add a new item for it; this ensures that + // the currently active prompt file is always available in the selection dialog, + // even if it is not included in the prompts list otherwise(from location setting) + if (!activeItem) { + activeItem = this._createPromptPickItem({ + uri: resource, + // "user" prompts are always registered in the prompts list, hence it + // should be safe to assume that `resource` is not "user" prompt here + storage: 'local', + type: 'instructions', + }); + fileOptions.push(activeItem); + } + + fileOptions.sort((file1, file2) => { + if (extUri.isEqual(file1.value, resource)) { + return -1; + } + + if (extUri.isEqual(file2.value, resource)) { + return 1; + } + + return 0; + }); + } + return fileOptions; + } + + private _createPromptPickItem(promptFile: IPromptPath): WithUriValue { + const { uri, storage } = promptFile; + const fileWithoutExtension = getCleanPromptName(uri); + + // if a "user" prompt, don't show its filesystem path in + // the user interface, but do that for all the "local" ones + const description = (storage === 'user') + ? localize( + 'user-prompt.capitalized', + 'User prompt', + ) + : this._labelService.getUriLabel(dirname(uri), { relative: true }); + + const tooltip = (storage === 'user') + ? description + : uri.fsPath; + + return { + id: uri.toString(), + type: 'item', + label: fileWithoutExtension, + description, + tooltip, + value: uri, + buttons: [EDIT_BUTTON, DELETE_BUTTON], + }; + } + + private async _handleButtonClick(quickPick: IQuickPick>, context: IQuickPickItemButtonEvent>) { + const { item, button } = context; + const { value } = item; + + // `edit` button was pressed, open the prompt file in editor + if (button === EDIT_BUTTON) { + return await this._openerService.open(value); + } + + // `delete` button was pressed, delete the prompt file + if (button === DELETE_BUTTON) { + // sanity check to confirm our expectations + assert( + (quickPick.activeItems.length < 2), + `Expected maximum one active item, got '${quickPick.activeItems.length}'.`, + ); + + const activeItem: WithUriValue | undefined = quickPick.activeItems[0]; + + // sanity checks - prompt file exists and is not a folder + const info = await this._fileService.stat(value); + assert( + info.isDirectory === false, + `'${value.fsPath}' points to a folder.`, + ); + + // don't close the main prompt selection dialog by the confirmation dialog + const previousIgnoreFocusOut = quickPick.ignoreFocusOut; + quickPick.ignoreFocusOut = true; + + const filename = getCleanPromptName(value); + const { confirmed } = await this._dialogService.confirm({ + message: localize( + 'commands.prompts.use.select-dialog.delete-prompt.confirm.message', + "Are you sure you want to delete '{0}'?", + filename, + ), + }); + + // restore the previous value of the `ignoreFocusOut` property + quickPick.ignoreFocusOut = previousIgnoreFocusOut; + + // if prompt deletion was not confirmed, nothing to do + if (!confirmed) { + return; + } + + // prompt deletion was confirmed so delete the prompt file + await this._fileService.del(value); + + // remove the deleted prompt from the selection dialog list + let removedIndex = -1; + quickPick.items = quickPick.items.filter((option, index) => { + if (option === item) { + removedIndex = index; + + return false; + } + + return true; + }); + + // if the deleted item was active item, find a new item to set as active + if (activeItem && (activeItem === item)) { + assert( + removedIndex >= 0, + 'Removed item index must be a valid index.', + ); + + // we set the previous item as new active, or the next item + // if removed prompt item was in the beginning of the list + const newActiveItemIndex = Math.max(removedIndex - 1, 0); + const newActiveItem: WithUriValue | undefined = quickPick.items[newActiveItemIndex]; + + quickPick.activeItems = newActiveItem ? [newActiveItem] : []; + } + + return; + } + + if (button === HELP_BUTTON) { + // open the documentation + await this._openerService.open(item.value); + return; + } + + throw new Error(`Unknown button '${JSON.stringify(button)}'.`); + } + +} + diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts similarity index 53% rename from src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts rename to src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts index 7d1f2ded5b9..ff002792618 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/attachPrompt.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/attachInstructions.ts @@ -6,16 +6,15 @@ import { IChatWidget, showChatView } from '../../../../../chat.js'; import { URI } from '../../../../../../../../../base/common/uri.js'; import { ACTION_ID_NEW_CHAT } from '../../../../chatClearActions.js'; -import { extUri } from '../../../../../../../../../base/common/resources.js'; import { assertDefined } from '../../../../../../../../../base/common/types.js'; -import { IChatAttachPromptActionOptions } from '../../../chatAttachPromptAction.js'; +import { IAttachInstructionsActionOptions } from '../../../chatAttachInstructionsAction.js'; import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; /** - * Options for the {@link attachPrompt} function. + * Options for the {@link attachInstructionsFiles} function. */ -export interface IAttachPromptOptions { +export interface IAttachOptions { /** * Chat widget instance to attach the prompt to. */ @@ -26,88 +25,56 @@ export interface IAttachPromptOptions { */ readonly inNewChat?: boolean; - /** - * Whether to skip attaching provided prompt if it is - * already attached as an implicit "current file" context. - */ - readonly skipIfImplicitlyAttached?: boolean; - readonly viewsService: IViewsService; readonly commandService: ICommandService; } /** - * Return value of the {@link attachPrompt} function. + * Return value of the {@link attachInstructionsFiles} function. */ interface IAttachResult { + /** + * Chat widget instance files were attached to. + */ readonly widget: IChatWidget; - readonly wasAlreadyAttached: boolean; + + /** + * List of instruction files that were already + * attached to the chat input. + */ + readonly alreadyAttached: readonly URI[]; } /** - * Check if provided uri is already attached to chat - * input as an implicit "current file" context. + * Attaches provided instructions to a chat input. */ -const isAttachedAsCurrentPrompt = ( - promptUri: URI, - widget: IChatWidget, -): boolean => { - const { implicitContext } = widget.input; - if (implicitContext === undefined) { - return false; - } - - if (implicitContext.isPrompt === false) { - return false; - } - - if (implicitContext.enabled === false) { - return false; - } - - assertDefined( - implicitContext.value, - 'Prompt value must always be defined.', - ); - - const uri = URI.isUri(implicitContext.value) - ? implicitContext.value - : implicitContext.value.uri; - - return extUri.isEqual(promptUri, uri); -}; - -/** - * Attaches provided prompts to a chat input. - */ -export const attachPrompt = async ( - file: URI, - options: IAttachPromptOptions, +export const attachInstructionsFiles = async ( + files: URI[], + options: IAttachOptions, ): Promise => { - const { skipIfImplicitlyAttached } = options; const widget = await getChatWidgetObject(options); - if (skipIfImplicitlyAttached && isAttachedAsCurrentPrompt(file, widget)) { - return { widget, wasAlreadyAttached: true }; + const alreadyAttached: URI[] = []; + + for (const file of files) { + if (widget.attachmentModel.promptInstructions.add(file)) { + alreadyAttached.push(file); + continue; + } } - const wasAlreadyAttached = widget - .attachmentModel - .promptInstructions - .add(file); - - return { widget, wasAlreadyAttached }; + return { widget, alreadyAttached }; }; /** - * Gets a chat widget based on the provided {@link IChatAttachPromptActionOptions.widget widget} + * Gets a chat widget based on the provided {@link IAttachInstructionsActionOptions.widget widget} * reference and the `inNewChat` flag. * * @throws if failed to reveal a chat widget. */ -const getChatWidgetObject = async ( - options: IAttachPromptOptions, +export const getChatWidgetObject = async ( + options: IAttachOptions, ): Promise => { const { widget, inNewChat } = options; @@ -122,10 +89,11 @@ const getChatWidgetObject = async ( }; /** - * Opens a chat session, or reveals an existing one. + * Reveals an existing one or creates a new one based on + * the provided `createNew` flag. */ const showChat = async ( - options: IAttachPromptOptions, + options: IAttachOptions, createNew: boolean = false, ): Promise => { const { commandService, viewsService } = options; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts similarity index 100% rename from src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts rename to src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/detachPrompt.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts new file mode 100644 index 00000000000..3d5acb6b4d8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/utils/runPrompt.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatWidget } from '../../../../../chat.js'; +import { getChatWidgetObject } from './attachInstructions.js'; +import { URI } from '../../../../../../../../../base/common/uri.js'; +import { basename } from '../../../../../../../../../base/common/resources.js'; +import { IViewsService } from '../../../../../../../../services/views/common/viewsService.js'; +import { ICommandService } from '../../../../../../../../../platform/commands/common/commands.js'; + +/** + * Options for the {@link runPromptFile} function. + */ +export interface IRunPromptOptions { + /** + * Chat widget instance to attach the prompt to. + */ + readonly widget?: IChatWidget; + /** + * Whether to create a new chat session and + * attach the instructions file to it. + */ + readonly inNewChat?: boolean; + + readonly viewsService: IViewsService; + readonly commandService: ICommandService; +} + +/** + * Return value of the {@link runPromptFile} function. + */ +interface IRunPromptResult { + readonly widget: IChatWidget; +} + +/** + * Runs the prompt file. + */ +export const runPromptFile = async ( + file: URI, + options: IRunPromptOptions, +): Promise => { + + const widget = await getChatWidgetObject(options); + + widget.setInput(`/${basename(file)}`); + // submit the prompt immediately + await widget.acceptInput(); + + + return { widget }; +}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts similarity index 65% rename from src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts rename to src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts index a5af698eb45..629a85d1ee8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/index.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/index.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { registerRunPromptActions } from './chatRunPromptAction.js'; -import { registerAttachPromptActions } from './chatAttachPromptAction.js'; +import { registerSaveToPromptActions } from './chatSaveToPromptAction.js'; +import { registerAttachPromptActions } from './chatAttachInstructionsAction.js'; +export { runAttachInstructionsAction } from './chatAttachInstructionsAction.js'; /** * Helper to register all actions related to reusable prompt files. */ -export const registerReusablePromptActions = () => { +export const registerPromptActions = () => { registerRunPromptActions(); registerAttachPromptActions(); + registerSaveToPromptActions(); }; - -export { runAttachPromptAction } from './chatAttachPromptAction.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts deleted file mode 100644 index 70160fb882d..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/chatAttachPromptAction.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CHAT_CATEGORY } from '../chatActions.js'; -import { localize2 } from '../../../../../../nls.js'; -import { ChatContextKeys } from '../../../common/chatContextKeys.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { IPromptsService } from '../../../common/promptSyntax/service/types.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; -import { IViewsService } from '../../../../../services/views/common/viewsService.js'; -import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; -import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { Action2, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { IQuickInputService } from '../../../../../../platform/quickinput/common/quickInput.js'; -import { attachPrompt, IAttachPromptOptions } from './dialogs/askToSelectPrompt/utils/attachPrompt.js'; -import { ISelectPromptOptions, askToSelectPrompt } from './dialogs/askToSelectPrompt/askToSelectPrompt.js'; - -/** - * Action ID for the `Attach Prompt` action. - */ -const ATTACH_PROMPT_ACTION_ID = 'workbench.action.chat.attach.prompt'; - -/** - * Options for the {@link AttachPromptAction} action. - */ -export interface IChatAttachPromptActionOptions extends Pick< - ISelectPromptOptions, 'resource' | 'widget' -> { - /** - * Whether to create a new chat panel or open - * an existing one (if present). - */ - inNewChat?: boolean; - - /** - * Whether to skip the prompt files selection dialog. - * - * Note! if this option is set to `true`, the {@link resource} - * option `must be defined`. - */ - skipSelectionDialog?: boolean; -} - -/** - * Action to attach a prompt to a chat widget input. - */ -class AttachPromptAction extends Action2 { - constructor() { - super({ - id: ATTACH_PROMPT_ACTION_ID, - title: localize2('workbench.action.chat.attach.prompt.label', "Use Prompt"), - f1: false, - precondition: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), - category: CHAT_CATEGORY, - }); - } - - public override async run( - accessor: ServicesAccessor, - options: IChatAttachPromptActionOptions, - ): Promise { - const fileService = accessor.get(IFileService); - const labelService = accessor.get(ILabelService); - const viewsService = accessor.get(IViewsService); - const openerService = accessor.get(IOpenerService); - const dialogService = accessor.get(IDialogService); - const promptsService = accessor.get(IPromptsService); - const commandService = accessor.get(ICommandService); - const quickInputService = accessor.get(IQuickInputService); - - const { skipSelectionDialog, resource } = options; - - if (skipSelectionDialog === true) { - assertDefined( - resource, - 'Resource must be defined when skipping prompt selection dialog.', - ); - - const attachOptions: IAttachPromptOptions = { - ...options, - viewsService, - commandService, - }; - - const { widget } = await attachPrompt( - resource, - attachOptions, - ); - - widget.focusInput(); - - return; - } - - // find all prompt files in the user workspace - const promptFiles = await promptsService.listPromptFiles(); - - await askToSelectPrompt({ - ...options, - promptFiles, - fileService, - viewsService, - labelService, - dialogService, - openerService, - commandService, - quickInputService, - }); - } -} - -/** - * Runs the `Attach Prompt` action with provided options. We export this - * function instead of {@link ATTACH_PROMPT_ACTION_ID} directly to - * encapsulate/enforce the correct options to be passed to the action. - */ -export const runAttachPromptAction = async ( - options: IChatAttachPromptActionOptions, - commandService: ICommandService, -): Promise => { - return await commandService.executeCommand( - ATTACH_PROMPT_ACTION_ID, - options, - ); -}; - -/** - * Helper to register the `Attach Prompt` action. - */ -export const registerAttachPromptActions = () => { - registerAction2(AttachPromptAction); -}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts deleted file mode 100644 index 3a8868e1e5b..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/askToSelectPrompt.ts +++ /dev/null @@ -1,205 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DOCS_OPTION } from './constants.js'; -import { IChatWidget } from '../../../../chat.js'; -import { attachPrompt } from './utils/attachPrompt.js'; -import { handleButtonClick } from './utils/handleButtonClick.js'; -import { URI } from '../../../../../../../../base/common/uri.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; -import { createPromptPickItem } from './utils/createPromptPickItem.js'; -import { createPlaceholderText } from './utils/createPlaceholderText.js'; -import { extUri } from '../../../../../../../../base/common/resources.js'; -import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { IPromptPath } from '../../../../../common/promptSyntax/service/types.js'; -import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; -import { IFileService } from '../../../../../../../../platform/files/common/files.js'; -import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; -import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; -import { IViewsService } from '../../../../../../../services/views/common/viewsService.js'; -import { IDialogService } from '../../../../../../../../platform/dialogs/common/dialogs.js'; -import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Options for the {@link askToSelectPrompt} function. - */ -export interface ISelectPromptOptions { - /** - * Prompt resource `URI` to attach to the chat input, if any. - * If provided the resource will be pre-selected in the prompt picker dialog, - * otherwise the dialog will show the prompts list without any pre-selection. - */ - readonly resource?: URI; - - /** - * Target chat widget reference to attach the prompt to. If the reference is - * provided, the command will attach the prompt to the input of the widget. - * Otherwise, the command will either create a new chat widget and or re-use - * an existing one, based on if user holds the `ctrl`/`cmd` key during prompt - * selection. - */ - readonly widget?: IChatWidget; - - /** - * List of prompt files to show in the selection dialog. - */ - readonly promptFiles: readonly IPromptPath[]; - - readonly fileService: IFileService; - readonly labelService: ILabelService; - readonly viewsService: IViewsService; - readonly openerService: IOpenerService; - readonly dialogService: IDialogService; - readonly commandService: ICommandService; - readonly quickInputService: IQuickInputService; -} - -/** - * Shows the prompt selection dialog to the user that allows to select a prompt file(s). - * - * If {@link ISelectPromptOptions.resource resource} is provided, the dialog will have - * the resource pre-selected in the prompts list. - */ -export const askToSelectPrompt = async ( - options: ISelectPromptOptions, -): Promise => { - const { promptFiles, resource, quickInputService, labelService } = options; - - const fileOptions = promptFiles.map((promptFile) => { - return createPromptPickItem(promptFile, labelService); - }); - - /** - * Add a link to the documentation to the end of prompts list. - */ - fileOptions.push(DOCS_OPTION); - - // if a resource is provided, create an `activeItem` for it to pre-select - // it in the UI, and sort the list so the active item appears at the top - let activeItem: WithUriValue | undefined; - if (resource) { - activeItem = fileOptions.find((file) => { - return extUri.isEqual(file.value, resource); - }); - - // if no item for the `resource` was found, it means that the resource is not - // in the list of prompt files, so add a new item for it; this ensures that - // the currently active prompt file is always available in the selection dialog, - // even if it is not included in the prompts list otherwise(from location setting) - if (!activeItem) { - activeItem = createPromptPickItem({ - uri: resource, - // "user" prompts are always registered in the prompts list, hence it - // should be safe to assume that `resource` is not "user" prompt here - type: 'local', - }, labelService); - fileOptions.push(activeItem); - } - - fileOptions.sort((file1, file2) => { - if (extUri.isEqual(file1.value, resource)) { - return -1; - } - - if (extUri.isEqual(file2.value, resource)) { - return 1; - } - - return 0; - }); - } - - /** - * If still no active item present, fall back to the first item in the list. - * This can happen only if command was invoked not from a focused prompt file - * (hence the `resource` is not provided in the options). - * - * Fixes the two main cases: - * - when no prompt files found it, pre-selects the documentation link - * - when there is only a single prompt file, pre-selects it - */ - if (!activeItem) { - activeItem = fileOptions[0]; - } - - // otherwise show the prompt file selection dialog - const quickPick = quickInputService.createQuickPick>(); - quickPick.activeItems = activeItem ? [activeItem] : []; - quickPick.placeholder = createPlaceholderText(options); - quickPick.canAcceptInBackground = true; - quickPick.matchOnDescription = true; - quickPick.items = fileOptions; - - const { openerService } = options; - return await new Promise(resolve => { - const disposables = new DisposableStore(); - - let lastActiveWidget = options.widget; - - // then the dialog is hidden or disposed for other reason, - // dispose everything and resolve the main promise - disposables.add({ - dispose() { - quickPick.dispose(); - resolve(); - // if something was attached (lastActiveWidget is set), focus on the target chat input - lastActiveWidget?.focusInput(); - }, - }); - - // handle the prompt `accept` event - disposables.add(quickPick.onDidAccept(async (event) => { - const { selectedItems } = quickPick; - const { keyMods } = quickPick; - - // sanity check to confirm our expectations - assert( - selectedItems.length === 1, - `Only one item can be accepted, got '${selectedItems.length}'.`, - ); - - const selectedOption = selectedItems[0]; - - // whether user selected the docs link option - const docsSelected = (selectedOption === DOCS_OPTION); - - // if documentation item was selected, open its link in a browser - if (docsSelected) { - // note that opening a file in editor also hides(disposes) the dialog - await openerService.open(selectedOption.value); - return; - } - - // otherwise attach the selected prompt to a chat input - const attachResult = await attachPrompt( - selectedOption.value, - { - ...options, - inNewChat: keyMods.ctrlCmd, - }, - ); - lastActiveWidget = attachResult.widget; - - // if user submitted their selection, close the dialog - if (!event.inBackground) { - disposables.dispose(); - } - })); - - // handle the `button click` event on a list item (edit, delete, etc.) - disposables.add(quickPick.onDidTriggerItemButton( - handleButtonClick.bind(null, { quickPick, ...options }), - )); - - // when the dialog is hidden, dispose everything - disposables.add(quickPick.onDidHide( - disposables.dispose.bind(disposables), - )); - - // finally, reveal the dialog - quickPick.show(); - }); -}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/constants.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/constants.ts deleted file mode 100644 index a7b1de1c72d..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/constants.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../nls.js'; -import { URI } from '../../../../../../../../base/common/uri.js'; -import { Codicon } from '../../../../../../../../base/common/codicons.js'; -import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { ThemeIcon } from '../../../../../../../../base/common/themables.js'; -import { DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; -import { isLinux, isWindows } from '../../../../../../../../base/common/platform.js'; -import { IQuickInputButton, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Name of the `"super"` key based on the current OS. - */ -export const SUPER_KEY_NAME = (isWindows || isLinux) ? 'Ctrl' : '⌘'; - -/** - * Name of the `alt`/`options` key based on the current OS. - */ -export const ALT_KEY_NAME = (isWindows || isLinux) ? 'Alt' : '⌥'; - -/** - * A special quick pick item that links to the documentation. - */ -export const DOCS_OPTION: WithUriValue = Object.freeze({ - type: 'item', - label: localize( - 'commands.prompts.use.select-dialog.docs-label', - 'Learn how to create reusable prompts', - ), - description: DOCUMENTATION_URL, - tooltip: DOCUMENTATION_URL, - value: URI.parse(DOCUMENTATION_URL), -}); - -/** - * Button that opens a prompt file in the editor. - */ -export const EDIT_BUTTON: IQuickInputButton = Object.freeze({ - tooltip: localize( - 'commands.prompts.use.select-dialog.open-button.tooltip', - "edit ({0}-key + enter)", - SUPER_KEY_NAME, - ), - iconClass: ThemeIcon.asClassName(Codicon.edit), -}); - -/** - * Button that deletes a prompt file. - */ -export const DELETE_BUTTON: IQuickInputButton = Object.freeze({ - tooltip: localize('delete', "delete"), - iconClass: ThemeIcon.asClassName(Codicon.trash), -}); diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts deleted file mode 100644 index 2cdfbc3d3c5..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { SUPER_KEY_NAME } from '../constants.js'; -import { localize } from '../../../../../../../../../nls.js'; -import { ISelectPromptOptions } from '../askToSelectPrompt.js'; - -/** - * Creates a placeholder text to show in the prompt selection dialog. - */ -export const createPlaceholderText = ( - options: ISelectPromptOptions, -): string => { - const { widget } = options; - - let text = localize( - 'commands.prompts.use.select-dialog.placeholder', - 'Select a prompt to use', - ); - - // if no widget reference is provided, add the note about the `ctrl`/`cmd` - // modifier that can be leveraged by users to alter the command behavior - if (widget === undefined) { - const superModifierNote = localize( - 'commands.prompts.use.select-dialog.super-modifier-note', - '{0}-key to use in new chat', - SUPER_KEY_NAME, - ); - - text += localize( - 'commands.prompts.use.select-dialog.modifier-notes', - ' (hold {0})', - superModifierNote, - ); - } - - return text; -}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts deleted file mode 100644 index 5203dddcd34..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../../nls.js'; -import { DELETE_BUTTON, EDIT_BUTTON } from '../constants.js'; -import { dirname } from '../../../../../../../../../base/common/resources.js'; -import { WithUriValue } from '../../../../../../../../../base/common/types.js'; -import { IPromptPath } from '../../../../../../common/promptSyntax/service/types.js'; -import { ILabelService } from '../../../../../../../../../platform/label/common/label.js'; -import { getCleanPromptName } from '../../../../../../../../../platform/prompts/common/constants.js'; -import { IQuickPickItem } from '../../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Creates a quick pick item for a prompt. - */ -export const createPromptPickItem = ( - promptFile: IPromptPath, - labelService: ILabelService, -): WithUriValue => { - const { uri, type } = promptFile; - const fileWithoutExtension = getCleanPromptName(uri); - - // if a "user" prompt, don't show its filesystem path in - // the user interface, but do that for all the "local" ones - const description = (type === 'user') - ? localize( - 'user-prompt.capitalized', - 'User prompt', - ) - : labelService.getUriLabel(dirname(uri), { relative: true }); - - const tooltip = (type === 'user') - ? description - : uri.fsPath; - - return { - id: uri.toString(), - type: 'item', - label: fileWithoutExtension, - description, - tooltip, - value: uri, - buttons: [EDIT_BUTTON, DELETE_BUTTON], - }; -}; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/handleButtonClick.ts b/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/handleButtonClick.ts deleted file mode 100644 index 51768640ad0..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/reusablePromptActions/dialogs/askToSelectPrompt/utils/handleButtonClick.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../../../../nls.js'; -import { DELETE_BUTTON, EDIT_BUTTON } from '../constants.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { WithUriValue } from '../../../../../../../../../base/common/types.js'; -import { IFileService } from '../../../../../../../../../platform/files/common/files.js'; -import { IOpenerService } from '../../../../../../../../../platform/opener/common/opener.js'; -import { IDialogService } from '../../../../../../../../../platform/dialogs/common/dialogs.js'; -import { getCleanPromptName } from '../../../../../../../../../platform/prompts/common/constants.js'; -import { IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent } from '../../../../../../../../../platform/quickinput/common/quickInput.js'; - -/** - * Options for the {@link handleButtonClick} function. - */ -interface IHandleButtonClickOptions { - quickPick: IQuickPick>; - fileService: IFileService; - openerService: IOpenerService; - dialogService: IDialogService; -} - -/** - * Handler for a button click event on a prompt file item in the prompt selection dialog. - */ -export async function handleButtonClick( - options: IHandleButtonClickOptions, - context: IQuickPickItemButtonEvent>, -) { - const { quickPick, openerService, fileService, dialogService } = options; - const { item, button } = context; - const { value } = item; - - // `edit` button was pressed, open the prompt file in editor - if (button === EDIT_BUTTON) { - return await openerService.open(value); - } - - // `delete` button was pressed, delete the prompt file - if (button === DELETE_BUTTON) { - // sanity check to confirm our expectations - assert( - (quickPick.activeItems.length < 2), - `Expected maximum one active item, got '${quickPick.activeItems.length}'.`, - ); - - const activeItem: WithUriValue | undefined = quickPick.activeItems[0]; - - // sanity checks - prompt file exists and is not a folder - const info = await fileService.stat(value); - assert( - info.isDirectory === false, - `'${value.fsPath}' points to a folder.`, - ); - - // don't close the main prompt selection dialog by the confirmation dialog - const previousIgnoreFocusOut = quickPick.ignoreFocusOut; - quickPick.ignoreFocusOut = true; - - const filename = getCleanPromptName(value); - const { confirmed } = await dialogService.confirm({ - message: localize( - 'commands.prompts.use.select-dialog.delete-prompt.confirm.message', - "Are you sure you want to delete '{0}'?", - filename, - ), - }); - - // restore the previous value of the `ignoreFocusOut` property - quickPick.ignoreFocusOut = previousIgnoreFocusOut; - - // if prompt deletion was not confirmed, nothing to do - if (!confirmed) { - return; - } - - // prompt deletion was confirmed so delete the prompt file - await fileService.del(value); - - // remove the deleted prompt from the selection dialog list - let removedIndex = -1; - quickPick.items = quickPick.items.filter((option, index) => { - if (option === item) { - removedIndex = index; - - return false; - } - - return true; - }); - - // if the deleted item was active item, find a new item to set as active - if (activeItem && (activeItem === item)) { - assert( - removedIndex >= 0, - 'Removed item index must be a valid index.', - ); - - // we set the previous item as new active, or the next item - // if removed prompt item was in the beginning of the list - const newActiveItemIndex = Math.max(removedIndex - 1, 0); - const newActiveItem: WithUriValue | undefined = quickPick.items[newActiveItemIndex]; - - quickPick.activeItems = newActiveItem ? [newActiveItem] : []; - } - - return; - } - - throw new Error(`Unknown button '${JSON.stringify(button)}'.`); -} diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 73b16e8fa5c..e620ed6bc7a 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -53,7 +53,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { dom.clearNode(this.domNode); this.renderDisposables.clear(); - const attachmentTypeName = (this.attachment.isPrompt === false) + const attachmentTypeName = (this.attachment.isPromptFile === false) ? localize('file.lowercase', "file") : localize('prompt.lowercase', "prompt"); @@ -73,7 +73,7 @@ export class ImplicitContextAttachmentWidget extends Disposable { const currentFileHint = currentFile + (this.attachment.enabled ? '' : ` (${inactive})`); const title = `${currentFileHint}\n${uriLabel}`; - const icon = (this.attachment.isPrompt) + const icon = this.attachment.isPromptFile ? ThemeIcon.fromId(Codicon.bookmark.id) : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts similarity index 72% rename from src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts rename to src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts index d05558a0489..dfad4bfdb19 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentsCollectionWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsCollectionWidget.ts @@ -6,34 +6,37 @@ import { URI } from '../../../../../../base/common/uri.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; -import { PromptAttachmentWidget } from './promptAttachmentWidget.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { InstructionsAttachmentWidget } from './promptInstructionsWidget.js'; +import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ChatPromptAttachmentsCollection } from '../../chatAttachmentModel/chatPromptAttachmentsCollection.js'; /** * Widget for a collection of prompt instructions attachments. - * See {@linkcode PromptAttachmentWidget}. + * See {@link InstructionsAttachmentWidget}. */ -export class PromptAttachmentsCollectionWidget extends Disposable { +export class PromptInstructionsAttachmentsCollectionWidget extends Disposable { /** * List of child instruction attachment widgets. */ - private children: PromptAttachmentWidget[] = []; + private children: InstructionsAttachmentWidget[] = []; /** * Event that fires when number of attachments change * - * See {@linkcode onAttachmentsCountChange}. + * See {@link onAttachmentsChange}. */ - private _onAttachmentsCountChange = this._register(new Emitter()); + private _onAttachmentsChange = this._register(new Emitter()); /** - * Subscribe to the `onAttachmentsCountChange` event. + * Subscribe to the `onAttachmentsChange` event. * @param callback Function to invoke when number of attachments change. */ - public onAttachmentsCountChange(callback: () => unknown): this { - this._register(this._onAttachmentsCountChange.event(callback)); + public onAttachmentsChange(callback: () => unknown): this { + this._register(this._onAttachmentsChange.event(callback)); return this; } @@ -59,6 +62,14 @@ export class PromptAttachmentsCollectionWidget extends Disposable { return this.model.chatAttachments; } + /** + * Get a promise that resolves when parsing/resolving processes + * are fully completed, including all possible nested child references. + */ + public allSettled() { + return this.model.allSettled(); + } + /** * Check if child widget list is empty (no attachments present). */ @@ -66,10 +77,23 @@ export class PromptAttachmentsCollectionWidget extends Disposable { return this.children.length === 0; } + /** + * Check if any of the attachments is a prompt file. + */ + public get hasPromptFile(): boolean { + return this.references.some((uri) => { + const model = this.modelService.getModel(uri); + const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri); + return languageId === PROMPT_LANGUAGE_ID; + }); + } + constructor( private readonly model: ChatPromptAttachmentsCollection, private readonly resourceLabels: ResourceLabels, @IInstantiationService private readonly initService: IInstantiationService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, @ILogService private readonly logService: ILogService, ) { super(); @@ -77,9 +101,9 @@ export class PromptAttachmentsCollectionWidget extends Disposable { this.render = this.render.bind(this); // when a new attachment model is added, create a new child widget for it - this.model.onAdd((attachment) => { + this._register(this.model.onAdd((attachment) => { const widget = this.initService.createInstance( - PromptAttachmentWidget, + InstructionsAttachmentWidget, attachment, this.resourceLabels, ); @@ -90,22 +114,22 @@ export class PromptAttachmentsCollectionWidget extends Disposable { // register the new child widget this.children.push(widget); - // if parent node is present - append the wiget to it, otherwise wait + // if parent node is present - append the widget to it, otherwise wait // until the `render` method will be called if (this.parentNode) { this.parentNode.appendChild(widget.domNode); } // fire the event to notify about the change in the number of attachments - this._onAttachmentsCountChange.fire(); - }); + this._onAttachmentsChange.fire(); + })); } /** * Handle child widget disposal. * @param widget The child widget that was disposed. */ - public handleAttachmentDispose(widget: PromptAttachmentWidget): this { + public handleAttachmentDispose(widget: InstructionsAttachmentWidget): this { // common prefix for all log messages const logPrefix = `[onChildDispose] Widget for instructions attachment '${widget.uri.path}'`; @@ -148,7 +172,7 @@ export class PromptAttachmentsCollectionWidget extends Disposable { this.parentNode?.removeChild(widget.domNode); // fire the event to notify about the change in the number of attachments - this._onAttachmentsCountChange.fire(); + this._onAttachmentsChange.fire(); return this; } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts similarity index 95% rename from src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts rename to src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts index ac4fcefcd20..4ff3586abaa 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/promptAttachments/promptAttachmentWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/promptInstructions/promptInstructionsWidget.ts @@ -31,7 +31,7 @@ import { getFlatContextMenuActions } from '../../../../../../platform/actions/br /** * Widget for a single prompt instructions attachment. */ -export class PromptAttachmentWidget extends Disposable { +export class InstructionsAttachmentWidget extends Disposable { /** * The root DOM node of the widget. */ @@ -106,12 +106,12 @@ export class PromptAttachmentWidget extends Disposable { const fileBasename = basename(file); const fileDirname = dirname(file); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = localize('chat.promptAttachment', "Prompt attachment, {0}", friendlyName); + const ariaLabel = localize('chat.instructionsAttachment', "Instructions attachment, {0}", friendlyName); const uriLabel = this.labelService.getUriLabel(file, { relative: true }); - const promptLabel = localize('prompt', "Prompt"); + const instructionsLabel = localize('instructions', "Instructions"); - let title = `${promptLabel} ${uriLabel}`; + let title = `${instructionsLabel} ${uriLabel}`; // if there are some errors/warning during the process of resolving // attachment references (including all the nested child references), @@ -144,7 +144,7 @@ export class PromptAttachmentWidget extends Disposable { this.domNode.ariaLabel = ariaLabel; this.domNode.tabIndex = 0; - const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, promptLabel)); + const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, instructionsLabel)); this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title)); // create the `remove` button diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 56220de100a..b79f4b81c5f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -48,7 +48,7 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; +import { PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; import { registerReusablePromptLanguageFeatures } from '../common/promptSyntax/languageFeatures/providers/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; import { IPromptsService } from '../common/promptSyntax/service/types.js'; @@ -101,8 +101,10 @@ import './contrib/chatInputEditorHover.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; import { LanguageModelToolsService } from './languageModelToolsService.js'; import './promptSyntax/contributions/createPromptCommand/createPromptCommand.js'; -import './promptSyntax/contributions/usePromptCommand.js'; +import './promptSyntax/contributions/attachInstructionsCommand.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; +import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './actions/promptActions/chatSaveToPromptAction.js'; +import { assertDefined } from '../../../../base/common/types.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -282,7 +284,7 @@ configurationRegistry.registerConfiguration({ 'chat.reusablePrompts.config.enabled.description', "Enable reusable prompt files (`*{0}`) in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).", PROMPT_FILE_EXTENSION, - DOCUMENTATION_URL, + PROMPT_DOCUMENTATION_URL, ), default: true, restricted: true, @@ -306,7 +308,7 @@ configurationRegistry.registerConfiguration({ 'chat.reusablePrompts.config.locations.description', "Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.", PROMPT_FILE_EXTENSION, - DOCUMENTATION_URL, + PROMPT_DOCUMENTATION_URL, ), default: { [PROMPT_FILES_DEFAULT_SOURCE_FOLDER]: true, @@ -488,7 +490,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @IChatSlashCommandService slashCommandService: IChatSlashCommandService, @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, - @IChatVariablesService chatVariablesService: IChatVariablesService, + @IChatWidgetService chatWidgetService: IChatWidgetService, @IInstantiationService instantiationService: IInstantiationService, ) { super(); @@ -501,6 +503,22 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { }, async () => { commandService.executeCommand(ACTION_ID_NEW_CHAT); })); + this._store.add(slashCommandService.registerSlashCommand({ + command: SAVE_TO_PROMPT_SLASH_COMMAND_NAME, + detail: nls.localize('save-chat-to-prompt-file', "Save chat to a prompt file"), + sortText: `z3_${SAVE_TO_PROMPT_SLASH_COMMAND_NAME}`, + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Panel] + }, async () => { + const { lastFocusedWidget } = chatWidgetService; + assertDefined( + lastFocusedWidget, + 'No currently active chat widget found.', + ); + + runSaveToPromptAction({ chat: lastFocusedWidget }, commandService); + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', detail: '', diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index f3de0984246..2dae13af3f4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -17,6 +17,13 @@ import { ISharedWebContentExtractorService } from '../../../../platform/webConte import { Schemas } from '../../../../base/common/network.js'; import { resolveImageEditorAttachContext } from './chatAttachmentResolve.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { equals } from '../../../../base/common/objects.js'; + +export interface IChatAttachmentChangeEvent { + readonly deleted: readonly string[]; + readonly added: readonly IChatRequestVariableEntry[]; + readonly updated: readonly IChatRequestVariableEntry[]; +} export class ChatAttachmentModel extends Disposable { /** @@ -34,9 +41,7 @@ export class ChatAttachmentModel extends Disposable { this.promptInstructions = this._register( this.initService.createInstance(ChatPromptAttachmentsCollection), - ).onUpdate(() => { - this._onDidChangeContext.fire(); - }); + ); } private _attachments = new Map(); @@ -44,8 +49,8 @@ export class ChatAttachmentModel extends Disposable { return Array.from(this._attachments.values()); } - protected _onDidChangeContext = this._register(new Emitter()); - readonly onDidChangeContext = this._onDidChangeContext.event; + private _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; get size(): number { return this._attachments.size; @@ -61,15 +66,23 @@ export class ChatAttachmentModel extends Disposable { } clear(): void { + const deleted = Array.from(this._attachments.keys()); this._attachments.clear(); - this._onDidChangeContext.fire(); + this._onDidChange.fire({ deleted, added: [], updated: [] }); } delete(...variableEntryIds: string[]) { + const deleted: string[] = []; + for (const variableEntryId of variableEntryIds) { - this._attachments.delete(variableEntryId); + if (this._attachments.delete(variableEntryId)) { + deleted.push(variableEntryId); + } + } + + if (deleted.length > 0) { + this._onDidChange.fire({ deleted, added: [], updated: [] }); } - this._onDidChangeContext.fire(); } async addFile(uri: URI, range?: IRange) { @@ -90,7 +103,6 @@ export class ChatAttachmentModel extends Disposable { value: uri, id: uri.toString(), name: basename(uri), - }); } @@ -118,22 +130,59 @@ export class ChatAttachmentModel extends Disposable { } addContext(...attachments: IChatRequestVariableEntry[]) { - let hasAdded = false; + const added: IChatRequestVariableEntry[] = []; for (const attachment of attachments) { if (!this._attachments.has(attachment.id)) { this._attachments.set(attachment.id, attachment); - hasAdded = true; + added.push(attachment); } } - if (hasAdded) { - this._onDidChangeContext.fire(); + if (added.length > 0) { + this._onDidChange.fire({ deleted: [], added, updated: [] }); } } clearAndSetContext(...attachments: IChatRequestVariableEntry[]) { - this.clear(); - this.addContext(...attachments); + const deleted = Array.from(this._attachments.keys()); + this._attachments.clear(); + + const added: IChatRequestVariableEntry[] = []; + for (const attachment of attachments) { + this._attachments.set(attachment.id, attachment); + added.push(attachment); + } + + if (deleted.length > 0 || added.length > 0) { + this._onDidChange.fire({ deleted, added, updated: [] }); + } + } + + updateContent(toDelete: Iterable, upsert: Iterable) { + const deleted: string[] = []; + const added: IChatRequestVariableEntry[] = []; + const updated: IChatRequestVariableEntry[] = []; + + for (const id of toDelete) { + if (this._attachments.delete(id)) { + deleted.push(id); + } + } + + for (const item of upsert) { + const oldItem = this._attachments.get(item.id); + if (!oldItem) { + this._attachments.set(item.id, item); + added.push(item); + } else if (!equals(oldItem, item)) { + this._attachments.set(item.id, item); + updated.push(item); + } + } + + if (deleted.length > 0 || added.length > 0 || updated.length > 0) { + this._onDidChange.fire({ deleted, added, updated }); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts index d3845fef1c8..e4a74a1e8f2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentModel.ts @@ -43,7 +43,7 @@ export class ChatPromptAttachmentModel extends Disposable { */ public get references(): readonly URI[] { const { reference } = this; - const { errorCondition } = this.reference; + const { errorCondition } = reference; // return no references if the attachment is disabled // or if this object itself has an error @@ -59,6 +59,19 @@ export class ChatPromptAttachmentModel extends Disposable { ]; } + /** + * Get list of all tools associated with the prompt. + * + * Note! This property returns pont-in-time state of the tools metadata + * and does not take into account if the prompt or its nested child + * references are still being resolved. Please use the {@link settled} + * or {@link allSettled} properties if you need to retrieve the final + * list of the tools available. + */ + public get toolsMetadata(): readonly string[] | null { + return this.reference.allToolsMetadata; + } + /** * Promise that resolves when the prompt is fully parsed, * including all its possible nested child references. @@ -79,7 +92,7 @@ export class ChatPromptAttachmentModel extends Disposable { * Event that fires when the error condition of the prompt * reference changes. * - * See {@linkcode onUpdate}. + * See {@link onUpdate}. */ protected _onUpdate = this._register(new Emitter()); /** @@ -95,7 +108,7 @@ export class ChatPromptAttachmentModel extends Disposable { /** * Event that fires when the object is disposed. * - * See {@linkcode onDispose}. + * See {@link onDispose}. */ protected _onDispose = this._register(new Emitter()); /** @@ -115,14 +128,15 @@ export class ChatPromptAttachmentModel extends Disposable { ) { super(); - this._onUpdate.fire = this._onUpdate.fire.bind(this._onUpdate); this._reference = this._register(this.initService.createInstance( BasePromptParser, this.getContentsProvider(uri), [], )); - this._reference.onUpdate(this._onUpdate.fire); + this._reference.onUpdate( + this._onUpdate.fire.bind(this._onUpdate), + ); } /** diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts index a1817f40e41..fcb37c09f12 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel/chatPromptAttachmentsCollection.ts @@ -6,13 +6,18 @@ import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; import { basename } from '../../../../../base/common/resources.js'; -import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatPromptAttachmentModel } from './chatPromptAttachmentModel.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IChatRequestVariableEntry, IPromptVariableEntry, isChatRequestFileEntry } from '../../common/chatModel.js'; + +/** + * Prefix for all prompt instruction variable IDs. + */ +const PROMPT_VARIABLE_ID_PREFIX = 'vscode.prompt.instructions'; /** * Prompt IDs start with a well-defined prefix that is used by @@ -27,7 +32,7 @@ export const createPromptVariableId = ( isRoot: boolean, ): string => { // the default prefix that is used for all prompt files - let prefix = 'vscode.prompt.instructions'; + let prefix = PROMPT_VARIABLE_ID_PREFIX; // if the reference is the root object, add the `.root` suffix if (isRoot) { prefix += '.root'; @@ -53,7 +58,7 @@ export const createPromptVariableId = ( export const toChatVariable = ( reference: Pick, isRoot: boolean, -): IChatRequestVariableEntry => { +): IPromptVariableEntry => { const { uri, isPromptFile } = reference; // default `id` is the stringified `URI` @@ -78,14 +83,56 @@ export const toChatVariable = ( value: uri, kind: 'file', modelDescription, + isRoot, }; }; +/** + * Checks of a provided chat variable is a `prompt file` variable. + */ +export function isPromptFileChatVariable( + variable: IChatRequestVariableEntry, +): variable is IPromptVariableEntry { + return isChatRequestFileEntry(variable) + && variable.id.startsWith(PROMPT_VARIABLE_ID_PREFIX); +} + /** * Model for a collection of prompt instruction attachments. * See {@linkcode ChatPromptAttachmentModel} for individual attachment. */ export class ChatPromptAttachmentsCollection extends Disposable { + /** + * Event that fires then this model is updated. + * + * See {@linkcode onUpdate}. + */ + protected _onUpdate = this._register(new Emitter()); + /** + * Subscribe to the `onUpdate` event. + */ + public onUpdate = this._onUpdate.event; + + /** + * Event that fires when a new prompt instruction attachment is added. + * See {@linkcode onAdd}. + */ + protected _onAdd = this._register(new Emitter()); + /** + * The `onAdd` event fires when a new prompt instruction attachment is added. + */ + public onAdd = this._onAdd.event; + + /** + * Event that fires when a new prompt instruction attachment is removed. + * See {@linkcode onRemove}. + */ + protected _onRemove = this._register(new Emitter()); + /** + * The `onRemove` event fires when a new prompt instruction attachment is removed. + */ + public onRemove = this._onRemove.event; + /** * List of all prompt instruction attachments. */ @@ -106,6 +153,26 @@ export class ChatPromptAttachmentsCollection extends Disposable { return result; } + /** + * Get list of tools associated with all attached prompt files. + */ + public get toolsMetadata(): readonly string[] | null { + const result = []; + + for (const child of this.attachments.values()) { + const { toolsMetadata } = child; + + if (toolsMetadata === null) { + continue; + } + + result.push(...toolsMetadata); + } + + // return unique list of all tools + return [...new Set(result)]; + } + /** * Get the list of all prompt instruction attachment variables, including all * nested child references of each attachment explicitly attached by user. @@ -144,7 +211,7 @@ export class ChatPromptAttachmentsCollection extends Disposable { * Promise that resolves when parsing of all attached prompt instruction * files completes, including parsing of all its possible child references. */ - public async allSettled(): Promise { + public async allSettled(): Promise { const attachments = [...this.attachments.values()]; await Promise.allSettled( @@ -152,36 +219,6 @@ export class ChatPromptAttachmentsCollection extends Disposable { return attachment.allSettled; }), ); - } - - /** - * Event that fires then this model is updated. - * - * See {@linkcode onUpdate}. - */ - protected _onUpdate = this._register(new Emitter()); - /** - * Subscribe to the `onUpdate` event. - * @param callback Function to invoke on update. - */ - public onUpdate(callback: () => unknown): this { - this._register(this._onUpdate.event(callback)); - - return this; - } - - /** - * Event that fires when a new prompt instruction attachment is added. - * See {@linkcode onAdd}. - */ - protected _onAdd = this._register(new Emitter()); - /** - * The `onAdd` event fires when a new prompt instruction attachment is added. - * - * @param callback Function to invoke on add. - */ - public onAdd(callback: (attachment: ChatPromptAttachmentModel) => unknown): this { - this._register(this._onAdd.event(callback)); return this; } @@ -214,14 +251,19 @@ export class ChatPromptAttachmentsCollection extends Disposable { // alternative results in an infinite loop of calling this callback this.attachments.deleteAndLeak(uri.path); this._onUpdate.fire(); + this._onRemove.fire(instruction); }); - this.attachments.set(uri.path, instruction); + // start resolving all references in the prompt instruction.resolve(); + this.attachments.set(uri.path, instruction); this._onAdd.fire(instruction); this._onUpdate.fire(); + // start resolving all references in the prompt + instruction.resolve(); + return false; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts new file mode 100644 index 00000000000..c042eecfc03 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatExtensionsContentPart.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatExtensionsContent.css'; +import * as dom from '../../../../../base/browser/dom.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ExtensionsList, getExtensions } from '../../../extensions/browser/extensionsViewer.js'; +import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; +import { IChatExtensionsContent } from '../../common/chatService.js'; +import { IChatRendererContent } from '../../common/chatViewModel.js'; +import { ChatTreeItem, ChatViewId, IChatCodeBlockInfo } from '../chat.js'; +import { IChatContentPart } from './chatContentParts.js'; +import { PagedModel } from '../../../../../base/common/paging.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; + +export class ChatExtensionsContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + private _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + public get codeblocks(): IChatCodeBlockInfo[] { + return []; + } + + public get codeblocksPartId(): string | undefined { + return undefined; + } + + constructor( + private readonly extensionsContent: IChatExtensionsContent, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + this.domNode = dom.$('.chat-extensions-content-part'); + const loadingElement = dom.append(this.domNode, dom.$('.loading-extensions-element')); + dom.append(loadingElement, dom.$(ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin'))), dom.$('span.loading-message', undefined, localize('chat.extensions.loading', 'Loading extensions...'))); + + const extensionsList = dom.append(this.domNode, dom.$('.extensions-list')); + const list = this._register(instantiationService.createInstance(ExtensionsList, extensionsList, ChatViewId, { alwaysConsumeMouseWheel: false }, { onFocus: Event.None, onBlur: Event.None, filters: {} })); + getExtensions(extensionsContent.extensions, extensionsWorkbenchService).then(extensions => { + loadingElement.remove(); + if (this._store.isDisposed) { + return; + } + list.setModel(new PagedModel(extensions)); + list.layout(); + this._onDidChangeHeight.fire(); + }); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return other.kind === 'extensions' && other.extensions.length === this.extensionsContent.extensions.length && other.extensions.every(ext => this.extensionsContent.extensions.includes(ext)); + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 322e9c65712..68aa1a45d3e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -45,6 +45,7 @@ import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions, localFileLangua import '../media/chatCodeBlockPill.css'; import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; +import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; const $ = dom.$; @@ -105,6 +106,11 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP hideEmptyCodeblock.style.display = 'none'; return hideEmptyCodeblock; } + if (languageId === 'vscode-extensions') { + const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') })); + this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + return chatExtensions.domNode; + } const globalIndex = globalCodeBlockIndexStart++; const thisPartIndex = thisPartCodeBlockIndexStart++; let textModel: Promise; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts index c0017f21f84..6c5c13e045e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatProgressContentPart.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append } from '../../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType } from '../../../../../base/browser/dom.js'; import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -111,6 +111,14 @@ export class ChatWorkingProgressContentPart extends ChatProgressContentPart impl new MarkdownString().appendText(localize('workingMessage', "Working...")) }; super(progressMessage, renderer, context, undefined, undefined, workingProgress.isPaused ? Codicon.debugPause : undefined, instantiationService, chatMarkdownAnchorService); + + if (workingProgress.isPaused) { + this.domNode.style.cursor = 'pointer'; + this.domNode.title = localize('resume', "Click to resume"); + this._register(addDisposableListener(this.domNode, EventType.CLICK, () => { + workingProgress.setPaused(false); + })); + } } override hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts index 65f1ab01e16..310130ec951 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTaskContentPart.ts @@ -18,6 +18,8 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart public readonly domNode: HTMLElement; public readonly onDidChangeHeight: Event; + private isSettled: boolean; + constructor( private readonly task: IChatTask, contentReferencesListPool: CollapsibleListPool, @@ -28,6 +30,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart super(); if (task.progress.length) { + this.isSettled = true; const refsPart = this._register(instantiationService.createInstance(ChatCollapsibleListContentPart, task.progress, task.content.value, context, contentReferencesListPool)); this.domNode = dom.$('.chat-progress-task'); this.domNode.appendChild(refsPart.domNode); @@ -35,6 +38,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart } else { // #217645 const isSettled = task.isSettled?.() ?? true; + this.isSettled = isSettled; const showSpinner = !isSettled && !context.element.isComplete; const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, showSpinner, true, undefined)); this.domNode = progressPart.domNode; @@ -45,7 +49,7 @@ export class ChatTaskContentPart extends Disposable implements IChatContentPart hasSameContent(other: IChatProgressRenderableResponseContent): boolean { return other.kind === 'progressTask' && other.progress.length === this.task.progress.length - && other.isSettled() === this.task.isSettled(); + && other.isSettled() === this.isSettled; } addDisposable(disposable: IDisposable): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css new file mode 100644 index 00000000000..f9be266abd3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatExtensionsContent.css @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-extensions-content-part { + border: 1px solid var(--vscode-chat-requestBorder); + border-bottom: none; + border-radius: 4px; +} + +.chat-extensions-content-part .extension-list-item { + border-bottom: 1px solid var(--vscode-chat-requestBorder); +} + +.chat-extensions-content-part .loading-extensions-element { + line-height: 18px; + padding: 4px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + user-select: none; + border-bottom: 1px solid var(--vscode-chat-requestBorder); +} + +.chat-extensions-content-part .loading-extensions-element .loading-message { + padding-left: 4px; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index 8a59e4f50e2..a5a53c49a68 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -628,8 +628,9 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito // DIFF editor const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName; const diffEditor = await this._editorService.openEditor({ - original: { resource: this._entry.originalURI, options: { selection: undefined } }, - modified: { resource: this._entry.modifiedURI, options: { selection } }, + original: { resource: this._entry.originalURI }, + modified: { resource: this._entry.modifiedURI }, + options: { selection }, label: defaultAgentName ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index d207ac76f99..8074b03af83 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -22,6 +22,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor, TEXT_DIFF_EDITOR_ID } from '../../../../common/editor.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { NOTEBOOK_CELL_LIST_FOCUSED } from '../../../notebook/common/notebookContextKeys.js'; abstract class ChatEditingEditorAction extends Action2 { @@ -80,7 +81,7 @@ abstract class NavigateAction extends ChatEditingEditorAction { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( ctxHasEditorModification, - EditorContextKeys.focus + ContextKeyExpr.or(EditorContextKeys.focus, NOTEBOOK_CELL_LIST_FOCUSED) ), }, f1: true, @@ -233,8 +234,8 @@ abstract class AcceptRejectHunkAction extends ChatEditingEditorAction { icon: _accept ? Codicon.check : Codicon.discard, f1: true, keybinding: { - when: EditorContextKeys.focus, - weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or(EditorContextKeys.focus, NOTEBOOK_CELL_LIST_FOCUSED), + weight: KeybindingWeight.WorkbenchContrib + 1, primary: _accept ? KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter : KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 3868469ddc4..e6d83e24c38 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -236,7 +236,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie const e_sum = this._edit; const e_ai = edit; this._edit = e_sum.compose(e_ai); - } else { // e_ai @@ -269,6 +268,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } this._allEditsAreFromUs = false; + this._userEditScheduler.schedule(); this._updateDiffInfoSeq(); const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; @@ -348,6 +348,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); + this._notifyAction('accepted'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); return true; @@ -366,6 +367,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); + this._notifyAction('rejected'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); return true; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 44cfbfa8293..67a0a3e60f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -81,6 +82,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im readonly abstract originalURI: URI; + protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifyAction('userModified'), 1000)); + constructor( readonly modifiedURI: URI, protected _telemetryInfo: IModifiedEntryTelemetryInfo, @@ -189,7 +192,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im protected abstract _doReject(tx: ITransaction | undefined): Promise; - private _notifyAction(outcome: 'accepted' | 'rejected') { + protected _notifyAction(outcome: 'accepted' | 'rejected' | 'userModified') { this._chatService.notifyUserAction({ action: { kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome }, agentId: this._telemetryInfo.agentId, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index 5eccb5c879b..cb5b7ea96e0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -409,7 +409,6 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } protected override async _doAccept(tx: ITransaction | undefined): Promise { - this.revertMarkdownPreviewStates(); this.updateCellDiffInfo([], tx); const snapshot = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService); restoreSnapshot(this.originalModel, snapshot); @@ -433,10 +432,6 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } } - private revertMarkdownPreviewStates() { - this.cellEntryMap.forEach(entry => !entry.disposed && entry.revertMarkdownPreviewState()); - } - protected override async _doReject(tx: ITransaction | undefined): Promise { this.updateCellDiffInfo([], tx); if (this.createdInRequestId === this._telemetryInfo.requestId) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 10cb4e02018..029acf012b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -220,6 +220,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic // multiple times during the process of response streaming. const editsSeen: ({ seen: number; streaming: IStreamingEdits } | undefined)[] = []; + let editorDidChange = false; + const editorListener = Event.once(this._editorService.onDidActiveEditorChange)(() => { + editorDidChange = true; + }); + const editedFilesExist = new ResourceMap>(); const ensureEditorOpen = (partUri: URI) => { const uri = CellUri.parse(partUri)?.notebook ?? partUri; @@ -233,8 +238,9 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return; } const activeUri = this._editorService.activeEditorPane?.input.resource; - const inactive = this._editorService.activeEditorPane?.input instanceof ChatEditorInput && this._editorService.activeEditorPane.input.sessionId === session.chatSessionId || - Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); + const inactive = editorDidChange + || this._editorService.activeEditorPane?.input instanceof ChatEditorInput && this._editorService.activeEditorPane.input.sessionId === session.chatSessionId + || Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); this._editorService.openEditor({ resource: uri, options: { inactive, preserveFocus: true, pinned: true } }); })); }; @@ -250,6 +256,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsSeen.length = 0; editedFilesExist.clear(); + editorListener.dispose(); }; const handleResponseParts = async () => { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts index dca50eb63ad..6fee0f48d05 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -186,6 +186,9 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } this.changeIndexComputer = new PrefixSumComputer(new Uint32Array(indexes)); + if (this.changeIndexComputer.getTotalSum() === 0) { + this.revertMarkupCellState(); + } })); // Build cell integrations (responsible for navigating changes within a cell and decorating cell text changes) @@ -426,7 +429,17 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I await this.notebookEditor.revealRangeInCenterAsync(cell, new Range(targetLines.startLineNumber, 0, targetLines.endLineNumberExclusive, 0)); } - blur(change: ICellDiffInfo | undefined) { + private revertMarkupCellState() { + for (const change of this.sortedCellChanges) { + const cellViewModel = this.getCellViewModel(change); + if (cellViewModel?.cellKind === CellKind.Markup && cellViewModel.getEditState() === CellEditState.Editing && + (cellViewModel.editStateSource === 'chatEditNavigation' || cellViewModel.editStateSource === 'chatEdit')) { + cellViewModel.updateEditState(CellEditState.Preview, 'chatEdit'); + } + } + } + + private blur(change: ICellDiffInfo | undefined) { if (!change) { return; } @@ -620,13 +633,13 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I } async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.fullName; - const diffInput = { - original: { resource: this._entry.originalURI, options: { selection: undefined } }, - modified: { resource: this._entry.modifiedURI, options: { selection: undefined } }, + const diffInput: IResourceDiffEditorInput = { + original: { resource: this._entry.originalURI }, + modified: { resource: this._entry.modifiedURI }, label: defaultAgentName ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) - } satisfies IResourceDiffEditorInput; + }; await this._editorService.openEditor(diffInput); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4ff4a92bfa6..d33d5a96808 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -14,7 +14,7 @@ import { Button } from '../../../../base/browser/ui/button/button.js'; import { IActionProvider } from '../../../../base/browser/ui/dropdown/dropdown.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction, Separator, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; +import { IAction } from '../../../../base/common/actions.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -49,7 +49,6 @@ import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOp import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -63,7 +62,6 @@ import { WorkbenchList } from '../../../../platform/list/browser/listService.js' import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { ResourceLabels } from '../../../browser/labels.js'; @@ -75,7 +73,6 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatEditingSession } from '../common/chatEditingService.js'; -import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js'; import { IChatRequestVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; import { IChatFollowup, IChatService } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; @@ -83,10 +80,10 @@ import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; -import { CancelAction, ChatEditingSessionSubmitAction, ChatSubmitAction, ChatSwitchToNextModelActionId, IChatExecuteActionContext, IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; +import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { AttachToolsAction } from './actions/chatToolActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; -import { PromptAttachmentsCollectionWidget } from './attachments/promptAttachments/promptAttachmentsCollectionWidget.js'; +import { PromptInstructionsAttachmentsCollectionWidget } from './attachments/promptInstructions/promptInstructionsCollectionWidget.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; @@ -102,6 +99,7 @@ import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileRefere import { ChatImplicitContext } from './contrib/chatImplicitContext.js'; import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; +import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; const $ = dom.$; @@ -162,7 +160,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly selectedToolsModel: ChatSelectedTools; - public getAttachedAndImplicitContext(sessionId: string): IChatRequestVariableEntry[] { + public async getAttachedAndImplicitContext(sessionId: string): Promise { const contextArr = [...this.attachmentModel.attachments]; if (this.implicitContext?.enabled && this.implicitContext.value) { contextArr.push(this.implicitContext.toBaseEntry()); @@ -184,18 +182,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ); } + // wait for all prompt files resolve precesses to settle + await this.promptInstructionsAttachmentsPart.allSettled(); + contextArr - .push(...this.instructionAttachmentsPart.chatAttachments); + .push(...this.promptInstructionsAttachmentsPart.chatAttachments); return contextArr; } /** - * Check if the chat input part has any prompt instruction attachments. + * Check if the chat input part has any prompt file attachments. */ - public get hasInstructionAttachments(): boolean { + public get hasPromptFileAttachments(): boolean { // if prompt attached explicitly as a "prompt" attachment - if (this.instructionAttachmentsPart.empty === false) { + if (this.promptInstructionsAttachmentsPart.hasPromptFile) { return true; } @@ -204,7 +205,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } // if prompt attached as an implicit "current file" context - return (this.implicitContext.isPrompt && this.implicitContext.enabled); + return (this.implicitContext.isPromptFile && this.implicitContext.enabled); } private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; @@ -285,9 +286,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Context key is set when prompt instructions are attached. */ - private promptInstructionsAttached: IContextKey; + private promptFileAttached: IContextKey; private chatMode: IContextKey; + private modelWidget: ModelPickerActionItem | undefined; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; @@ -340,9 +342,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Child widget of prompt instruction attachments. - * See {@linkcode PromptAttachmentsCollectionWidget}. + * See {@linkcode PromptInstructionsAttachmentsCollectionWidget}. */ - private instructionAttachmentsPart: PromptAttachmentsCollectionWidget; + private promptInstructionsAttachmentsPart: PromptInstructionsAttachmentsCollectionWidget; constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used @@ -388,7 +390,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); - this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); + this.promptFileAttached = ChatContextKeys.hasPromptFile.bindTo(contextKeyService); this.chatMode = ChatContextKeys.chatMode.bindTo(contextKeyService); this.history = this.loadHistory(); @@ -404,17 +406,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); - this.instructionAttachmentsPart = this._register( + this.promptInstructionsAttachmentsPart = this._register( instantiationService.createInstance( - PromptAttachmentsCollectionWidget, + PromptInstructionsAttachmentsCollectionWidget, this.attachmentModel.promptInstructions, this._contextResourceLabels, ), ); // trigger re-layout of chat input when number of instruction attachment changes - this.instructionAttachmentsPart.onAttachmentsCountChange(() => { - this._onDidChangeHeight.fire(); + this.promptInstructionsAttachmentsPart.onAttachmentsChange(() => { + this._handleAttachedContextChange(); }); this.initSelectedModel(); @@ -431,22 +433,34 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return `chat.currentLanguageModel.${this.location}`; } + private getSelectedModelIsDefaultStorageKey(): string { + return `chat.currentLanguageModel.${this.location}.isDefault`; + } + private initSelectedModel() { const persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); + const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'github.copilot-chat/gpt-4o'); + if (persistedSelection) { const model = this.languageModelsService.lookupLanguageModel(persistedSelection); if (model) { - this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection }); - this.checkModelSupported(); + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || model.isDefault) { + this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection }); + this.checkModelSupported(); + } } else { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { const persistedModel = e.added?.find(m => m.identifier === persistedSelection); if (persistedModel) { this._waitForPersistedLanguageModel.clear(); - if (persistedModel.metadata.isUserSelectable) { - this.setCurrentLanguageModel({ metadata: persistedModel.metadata, identifier: persistedSelection }); - this.checkModelSupported(); + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || persistedModel.metadata.isDefault) { + if (persistedModel.metadata.isUserSelectable) { + this.setCurrentLanguageModel({ metadata: persistedModel.metadata, identifier: persistedSelection }); + this.checkModelSupported(); + } } } }); @@ -472,6 +486,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + public openModelPicker(): void { + this.modelWidget?.show(); + } + private checkModelSupported(): void { if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { this.setCurrentLanguageModelToDefault(); @@ -537,6 +555,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefault, StorageScope.APPLICATION, StorageTarget.USER); this._onDidChangeCurrentLanguageModel.fire(model); } @@ -877,7 +896,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.renderAttachedContext(); - this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange())); + this._register(this._attachmentModel.onDidChange(() => this._handleAttachedContextChange())); this.renderChatEditingSessionState(null); if (this.options.renderWorkingSet) { @@ -991,23 +1010,26 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - if (action.id === ChatSwitchToNextModelActionId && action instanceof MenuItemAction) { + if (action.id === ChatOpenModelPickerActionId && action instanceof MenuItemAction) { if (!this._currentLanguageModel) { this.setCurrentLanguageModelToDefault(); } if (this._currentLanguageModel) { - const itemDelegate: ModelPickerDelegate = { + const itemDelegate: IModelPickerDelegate = { onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { // The user changed the language model, so we don't wait for the persisted option to be registered this._waitForPersistedLanguageModel.clear(); this.setCurrentLanguageModel(model); this.renderAttachedContext(); + + // TODO- remove with https://github.com/microsoft/vscode/issues/246987 + this.focus(); }, getModels: () => this.getModels() }; - return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate); + return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, itemDelegate); } } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { const delegate: IModePickerDelegate = { @@ -1125,7 +1147,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const hoverDelegate = store.add(createInstantHoverDelegate()); const attachments = [...this.attachmentModel.attachments.entries()]; - const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value) || this.hasInstructionAttachments; + const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value) || this.hasPromptFileAttachments; dom.setVisibility(Boolean(hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer); dom.setVisibility(hasAttachments, this.attachedContextContainer); if (!attachments.length) { @@ -1137,8 +1159,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge container.appendChild(implicitPart.domNode); } - this.promptInstructionsAttached.set(this.hasInstructionAttachments); - this.instructionAttachmentsPart.render(container); + this.promptFileAttached.set(this.hasPromptFileAttachments); + this.promptInstructionsAttachmentsPart.render(container); for (const [index, attachment] of attachments) { const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; @@ -1495,90 +1517,6 @@ class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem { } } -interface ModelPickerDelegate { - onDidChangeModel: Event; - setModel(selectedModelId: ILanguageModelChatMetadataAndIdentifier): void; - getModels(): ILanguageModelChatMetadataAndIdentifier[]; -} - -class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding { - constructor( - action: MenuItemAction, - private currentLanguageModel: ILanguageModelChatMetadataAndIdentifier, - private readonly delegate: ModelPickerDelegate, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatEntitlementService chatEntitlementService: IChatEntitlementService, - @ICommandService commandService: ICommandService, - @IMenuService menuService: IMenuService, - @ITelemetryService telemetryService: ITelemetryService, - ) { - const modelActionsProvider: IActionProvider = { - getActions: () => { - const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { - return { - id: entry.identifier, - label: entry.metadata.name, - tooltip: '', - class: undefined, - enabled: true, - checked: entry.identifier === this.currentLanguageModel.identifier, - run: () => { - this.currentLanguageModel = entry; - this.renderLabel(this.element!); - this.delegate.setModel(entry); - } - }; - }; - - const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); - const actions = models.map(entry => setLanguageModelAction(entry)); - - // Add menu contributions from extensions - const menuActions = menuService.getMenuActions(MenuId.ChatModelPicker, contextKeyService); - const menuContributions = getFlatActionBarActions(menuActions); - if (menuContributions.length > 0 || chatEntitlementService.entitlement === ChatEntitlement.Limited) { - actions.push(new Separator()); - } - actions.push(...menuContributions); - if (chatEntitlementService.entitlement === ChatEntitlement.Limited) { - actions.push(toAction({ - id: 'moreModels', label: localize('chat.moreModels', "Add more Models"), run: () => { - const commandId = 'workbench.action.chat.upgradePlan'; - telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-models' }); - commandService.executeCommand(commandId); - } - })); - } - return actions; - } - }; - - const actionWithLabel: IAction = { - ...action, - tooltip: localize('chat.modelPicker.label', "Pick Model"), - run: () => { } - }; - super(actionWithLabel, modelActionsProvider, contextMenuService, undefined, keybindingService, contextKeyService); - this._register(delegate.onDidChangeModel(modelId => { - this.currentLanguageModel = modelId; - this.renderLabel(this.element!); - })); - } - - protected override renderLabel(element: HTMLElement): IDisposable | null { - this.setAriaLabelAttributes(element); - dom.reset(element, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); - return null; - } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('chat-modelPicker-item'); - } -} - const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 0f6194ed65a..8dc6c9c7fae 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -48,7 +48,7 @@ import { IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { getNWords } from '../common/chatWordCounter.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; @@ -62,6 +62,7 @@ import { ChatCodeCitationContentPart } from './chatContentParts/chatCodeCitation import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandContentPart.js'; import { ChatConfirmationContentPart } from './chatContentParts/chatConfirmationContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts/chatContentParts.js'; +import { ChatExtensionsContentPart } from './chatContentParts/chatExtensionsContentPart.js'; import { ChatMarkdownContentPart, EditorPool } from './chatContentParts/chatMarkdownContentPart.js'; import { ChatProgressContentPart, ChatWorkingProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart.js'; @@ -817,7 +818,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer element.model.setPaused(p) }); } return { content: partsToRender, moreContentAvailable }; @@ -901,6 +902,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); + return part; + } + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { if (!isResponseVM(context.element)) { return; diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 3b763727a52..d87cc9c4afa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -20,7 +20,7 @@ import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { contentRefUrl } from '../common/annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../common/chatColors.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; import { IChatMarkdownContent, IChatService } from '../common/chatService.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; @@ -112,8 +112,9 @@ export class ChatMarkdownDecorationsRenderer { const title = uri ? this.labelService.getUriLabel(uri, { relative: true }) : part instanceof ChatRequestSlashCommandPart ? part.slashCommand.detail : part instanceof ChatRequestAgentSubcommandPart ? part.command.description : - part instanceof ChatRequestToolPart ? (this.toolsService.getTool(part.toolId)?.userDescription) : - ''; + part instanceof ChatRequestSlashPromptPart ? part.slashPromptCommand.command : + part instanceof ChatRequestToolPart ? (this.toolsService.getTool(part.toolId)?.userDescription) : + ''; const args: IDecorationWidgetArgs = { title }; const text = part.text; diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 5c96d139e1a..7ca30ffa337 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -32,7 +32,6 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation const verifiedWidget: IChatWidget = widget; const focusedItem = verifiedWidget.getFocus(); - if (!focusedItem) { return; } @@ -65,6 +64,19 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi if (!responseContent && 'errorDetails' in item && item.errorDetails) { responseContent = item.errorDetails.message; } + if (isResponseVM(item)) { + const toolInvocation = item.response.value.find(item => item.kind === 'toolInvocation'); + if (toolInvocation?.confirmationMessages) { + const title = toolInvocation.confirmationMessages.title; + const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : toolInvocation.confirmationMessages.message.value; + const terminalCommand = toolInvocation.toolSpecificData && 'command' in toolInvocation.toolSpecificData ? toolInvocation.toolSpecificData.command : undefined; + responseContent += `${title}`; + if (terminalCommand) { + responseContent += `: ${terminalCommand}`; + } + responseContent += `\n${message}`; + } + } return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts index 53b7d6b3411..e0ef195e48c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { reset } from '../../../../base/browser/dom.js'; import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; @@ -43,6 +42,8 @@ export class ChatSelectedTools extends Disposable { readonly toolsActionItemViewItemProvider: IActionViewItemProvider & { onDidRender: Event }; + private allTools: IObservable[]>; + constructor( @ILanguageModelToolsService toolsService: ILanguageModelToolsService, @IInstantiationService instaService: IInstantiationService, @@ -52,7 +53,7 @@ export class ChatSelectedTools extends Disposable { this._selectedTools = this._register(storedTools(StorageScope.WORKSPACE, StorageTarget.MACHINE, storageService)); - const allTools = observableFromEvent( + this.allTools = observableFromEvent( toolsService.onDidChangeTools, () => Array.from(toolsService.getTools()).filter(t => t.supportsToolPicker) ); @@ -66,7 +67,7 @@ export class ChatSelectedTools extends Disposable { this.tools = derived(r => { const disabled = disabledData.read(r); - const tools = allTools.read(r); + const tools = this.allTools.read(r); if (!disabled) { return tools; } @@ -77,7 +78,7 @@ export class ChatSelectedTools extends Disposable { }); const toolsCount = derived(r => { - const count = allTools.read(r).length; + const count = this.allTools.read(r).length; const enabled = this.tools.read(r).length; return { count, enabled }; }); @@ -125,6 +126,24 @@ export class ChatSelectedTools extends Disposable { ); } + /** + * Select only the provided tools unselecting the rest. + * + * @param tools Set of tool IDs to select. + */ + public selectOnly( + tools: readonly string[], + ): void { + const allTools = this.allTools.get(); + const uniqueTools = new Set(tools); + + const disabledTools = allTools.filter((tool) => { + return (uniqueTools.has(tool.id) === false); + }); + + this.update([], disabledTools); + } + update(disableBuckets: readonly ToolDataSource[], disableTools: readonly IToolData[]): void { this._selectedTools.set({ disabledBuckets: disableBuckets.map(ToolDataSource.toKey), diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 6c14a25e1a1..7b15fa89f3f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -56,7 +56,7 @@ import { IExtensionsWorkbenchService } from '../../extensions/common/extensions. import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; -import { IChatRequestModel } from '../common/chatModel.js'; +import { IChatRequestModel, ChatRequestModel, ChatModel, IChatRequestVariableData, IChatRequestToolEntry } from '../common/chatModel.js'; import { IChatProgress, IChatService } from '../common/chatService.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js'; import { ILanguageModelsService } from '../common/languageModels.js'; @@ -64,6 +64,9 @@ import { CHAT_CATEGORY, CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from './acti import { ChatViewId, IChatWidgetService, showCopilotView } from './chat.js'; import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import './media/chatSetup.css'; +import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -94,9 +97,9 @@ const ToolsAgentWhen = ContextKeyExpr.and( ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension ); -class SetupChatAgentImplementation extends Disposable implements IChatAgentImplementation { +class SetupChatAgent extends Disposable implements IChatAgentImplementation { - static register(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { disposable: IDisposable; agent: SetupChatAgentImplementation } { + static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { disposable: IDisposable; agent: SetupChatAgent } { return instantiationService.invokeFunction(accessor => { const chatAgentService = accessor.get(IChatAgentService); @@ -125,34 +128,43 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple break; } - const disposable = new DisposableStore(); - - disposable.add(chatAgentService.registerAgent(id, { - id, - name: `${defaultChat.providerName} Copilot`, - isDefault: true, - isCore: true, - modes: mode ? [mode] : [ChatMode.Ask], - when: mode === ChatMode.Agent ? ToolsAgentWhen?.serialize() : undefined, - slashCommands: [], - disambiguation: [], - locations: [location], - metadata: { - helpTextPrefix: SetupChatAgentImplementation.SETUP_NEEDED_MESSAGE - }, - description, - extensionId: nullExtensionDescription.identifier, - extensionDisplayName: nullExtensionDescription.name, - extensionPublisherId: nullExtensionDescription.publisher - })); - - const agent = disposable.add(instantiationService.createInstance(SetupChatAgentImplementation, context, controller, location)); - disposable.add(chatAgentService.registerAgentImplementation(id, agent)); - - return { agent, disposable }; + return SetupChatAgent.registerAgents(instantiationService, chatAgentService, id, `${defaultChat.providerName} Copilot`, true, description, location, mode, context, controller); }); } + static registerOtherAgents(instantiationService: IInstantiationService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { disposable: IDisposable; agent: SetupChatAgent } { + return instantiationService.invokeFunction(accessor => { + const chatAgentService = accessor.get(IChatAgentService); + return SetupChatAgent.registerAgents(instantiationService, chatAgentService, id, name, isDefault, description, location, mode, context, controller); + }); + } + + static registerAgents(instantiationService: IInstantiationService, chatAgentService: IChatAgentService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatMode | undefined, context: ChatEntitlementContext, controller: Lazy): { disposable: IDisposable; agent: SetupChatAgent } { + + const disposables = new DisposableStore(); + disposables.add(chatAgentService.registerAgent(id, { + id, + name, + isDefault, + isCore: true, + modes: mode ? [mode] : [ChatMode.Ask], + when: mode === ChatMode.Agent ? ToolsAgentWhen?.serialize() : undefined, + slashCommands: [], + disambiguation: [], + locations: [location], + metadata: { helpTextPrefix: SetupChatAgent.SETUP_NEEDED_MESSAGE }, + description, + extensionId: nullExtensionDescription.identifier, + extensionDisplayName: nullExtensionDescription.name, + extensionPublisherId: nullExtensionDescription.publisher + })); + + const agent = disposables.add(instantiationService.createInstance(SetupChatAgent, context, controller, location)); + disposables.add(chatAgentService.registerAgentImplementation(id, agent)); + + return { agent, disposable: disposables }; + } + private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up Copilot to use Chat.")); private readonly _onUnresolvableError = this._register(new Emitter()); @@ -173,25 +185,25 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void): Promise { - return this.instantiationService.invokeFunction(async accessor => { - const chatService = accessor.get(IChatService); // use accessor for lazy loading - const languageModelsService = accessor.get(ILanguageModelsService); // of chat related services + return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => { + const chatService = accessor.get(IChatService); + const languageModelsService = accessor.get(ILanguageModelsService); const chatWidgetService = accessor.get(IChatWidgetService); const chatAgentService = accessor.get(IChatAgentService); - - return this.doInvoke(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + const languageModelToolsService = accessor.get(ILanguageModelToolsService); + return this.doInvoke(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); }); } - private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Available || this.context.state.entitlement === ChatEntitlement.Unknown) { - return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); } - return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService); + return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService); } - private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1); if (!requestModel) { this.logService.error('[chat setup] Request model not found, cannot redispatch request.'); @@ -203,14 +215,14 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple content: new MarkdownString(localize('waitingCopilot', "Getting Copilot ready.")), }); - await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); return {}; } - private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { + private async forwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { try { - await this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + await this.doForwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); } catch (error) { progress({ kind: 'warning', @@ -219,12 +231,12 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } } - private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { + private async doForwardRequestToCopilot(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { if (this.pendingForwardedRequests.has(requestModel.session.sessionId)) { throw new Error('Request already in progress'); } - const forwardRequest = this.doForwardRequestToCopilotWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + const forwardRequest = this.doForwardRequestToCopilotWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); this.pendingForwardedRequests.set(requestModel.session.sessionId, forwardRequest); try { @@ -234,7 +246,7 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } } - private async doForwardRequestToCopilotWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService): Promise { + private async doForwardRequestToCopilotWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise { const widget = chatWidgetService.getWidgetBySessionId(requestModel.session.sessionId); const mode = widget?.input.currentMode; const languageModel = widget?.input.currentLanguageModel; @@ -245,8 +257,9 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService); const whenAgentReady = this.whenAgentReady(chatAgentService, mode); + const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel); - if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise) { + if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) { const timeoutHandle = setTimeout(() => { progress({ kind: 'progressMessage', @@ -258,7 +271,7 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple const ready = await Promise.race([ timeout(20000).then(() => 'timedout'), this.whenDefaultAgentFailed(chatService).then(() => 'error'), - Promise.allSettled([whenLanguageModelReady, whenAgentReady]) + Promise.allSettled([whenLanguageModelReady, whenAgentReady, whenToolsModelReady]) ]); if (ready === 'error' || ready === 'timedout') { @@ -294,6 +307,30 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, e => e.added?.some(added => added.metadata.isDefault) ?? false)); } + private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise | void { + + const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart); + if (!needsToolsModel) { + return; // No tools in this request, no need to check + } + + // check that tools other than setup. and internal tools are registered. + for (const tool of languageModelToolsService.getTools()) { + if (tool.source.type !== 'internal') { + return; // we have tools! + } + } + + return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, (_) => { + for (const tool of languageModelToolsService.getTools()) { + if (tool.source.type !== 'internal') { + return true; // we have tools! + } + } + return false; // no external tools found + })); + } + private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatMode | undefined): Promise | void { const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode); if (defaultAgent && !defaultAgent.isCore) { @@ -312,7 +349,7 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple }); } - private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService): Promise { + private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' }); const requestModel = chatWidgetService.getWidgetBySessionId(request.sessionId)?.viewModel?.model.getRequests().at(-1); @@ -334,9 +371,9 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } })); - let success = undefined; + let result: IChatSetupResult | undefined = undefined; try { - success = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run(); + result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run(); } catch (error) { this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`); } finally { @@ -344,10 +381,19 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple } // User has agreed to run the setup - if (typeof success === 'boolean') { - if (success) { - if (requestModel) { - await this.forwardRequestToCopilot(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService); + if (typeof result?.success === 'boolean') { + if (result.success) { + if (result.dialogSkipped) { + progress({ + kind: 'progressMessage', + content: new MarkdownString(localize('copilotSetupSuccess', "Copilot setup finished successfully.")) + }); + } else if (requestModel) { + // Replace agent part with the actual Copilot agent + let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); + // Then replace any tool parts with the actual Copilot tools + newRequest = this.replaceToolInRequestModel(newRequest); + await this.forwardRequestToCopilot(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService); } } else { progress({ @@ -361,12 +407,132 @@ class SetupChatAgentImplementation extends Disposable implements IChatAgentImple else { progress({ kind: 'markdownContent', - content: SetupChatAgentImplementation.SETUP_NEEDED_MESSAGE, + content: SetupChatAgent.SETUP_NEEDED_MESSAGE, }); } return {}; } + + private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel { + const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + if (!agentPart) { + return requestModel; + } + + const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase()); + const githubAgent = chatAgentService.getAgent(agentId); + if (!githubAgent) { + return requestModel; + } + + const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent); + + return new ChatRequestModel({ + session: requestModel.session as ChatModel, + message: { + parts: requestModel.message.parts.map(part => { + if (part instanceof ChatRequestAgentPart) { + return newAgentPart; + } + return part; + }), + text: requestModel.message.text + }, + variableData: requestModel.variableData, + timestamp: Date.now(), + attempt: requestModel.attempt, + confirmation: requestModel.confirmation, + locationData: requestModel.locationData, + attachedContext: requestModel.attachedContext, + isCompleteAddedRequest: requestModel.isCompleteAddedRequest, + }); + } + + private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel { + const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart); + if (!toolPart) { + return requestModel; + } + + const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase()); + const newToolPart = new ChatRequestToolPart( + toolPart.range, + toolPart.editorRange, + toolPart.toolName, + toolId, + toolPart.displayName, + toolPart.icon + ); + + const chatRequestToolEntry: IChatRequestToolEntry = { + id: toolId, + name: 'new', + range: toolPart.range, + kind: 'tool', + value: undefined + }; + + const variableData: IChatRequestVariableData = { + variables: [chatRequestToolEntry] + }; + + return new ChatRequestModel({ + session: requestModel.session as ChatModel, + message: { + parts: requestModel.message.parts.map(part => { + if (part instanceof ChatRequestToolPart) { + return newToolPart; + } + return part; + }), + text: requestModel.message.text + }, + variableData: variableData, + timestamp: Date.now(), + attempt: requestModel.attempt, + confirmation: requestModel.confirmation, + locationData: requestModel.locationData, + attachedContext: [chatRequestToolEntry], + isCompleteAddedRequest: requestModel.isCompleteAddedRequest, + }); + } +} + + +class SetupTool extends Disposable implements IToolImpl { + + static registerTools(instantiationService: IInstantiationService, toolData: IToolData): { disposable: IDisposable; tool: SetupTool } { + return instantiationService.invokeFunction(accessor => { + const disposables = new DisposableStore(); + const toolService = accessor.get(ILanguageModelToolsService); + disposables.add(toolService.registerToolData(toolData)); + const tool = instantiationService.createInstance(SetupTool); + disposables.add(toolService.registerToolImplementation(toolData.id, tool)); + return { tool, disposable: disposables }; + }); + } + + constructor( + ) { + super(); + } + + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + const result: IToolResult = { + content: [ + { + kind: 'text', + value: '' + } + ] + }; + return Promise.resolve(result); + } + + prepareToolInvocation?(parameters: any, token: CancellationToken): Promise { + return Promise.resolve(undefined); + } } enum ChatSetupStrategy { @@ -376,6 +542,11 @@ enum ChatSetupStrategy { SetupWithEnterpriseProvider = 3 } +interface IChatSetupResult { + readonly success: boolean | undefined; + readonly dialogSkipped: boolean; +} + class ChatSetup { private static instance: ChatSetup | undefined = undefined; @@ -383,14 +554,16 @@ class ChatSetup { let instance = ChatSetup.instance; if (!instance) { instance = ChatSetup.instance = instantiationService.invokeFunction(accessor => { - return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IContextMenuService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService)); + return new ChatSetup(context, controller, instantiationService, accessor.get(ITelemetryService), accessor.get(IContextMenuService), accessor.get(IWorkbenchLayoutService), accessor.get(IKeybindingService), accessor.get(IChatEntitlementService), accessor.get(ILogService), accessor.get(IConfigurationService)); }); } return instance; } - private pendingRun: Promise | undefined = undefined; + private pendingRun: Promise | undefined = undefined; + + private skipDialogOnce = false; private constructor( private readonly context: ChatEntitlementContext, @@ -402,9 +575,14 @@ class ChatSetup { @IKeybindingService private readonly keybindingService: IKeybindingService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { } - async run(): Promise { + skipDialog(): void { + this.skipDialogOnce = true; + } + + async run(): Promise { if (this.pendingRun) { return this.pendingRun; } @@ -418,15 +596,18 @@ class ChatSetup { } } - private async doRun(): Promise { + private async doRun(): Promise { + const dialogSkipped = this.skipDialogOnce; + this.skipDialogOnce = false; + let setupStrategy: ChatSetupStrategy; - if (this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { + if (dialogSkipped || this.chatEntitlementService.entitlement === ChatEntitlement.Pro || this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { setupStrategy = ChatSetupStrategy.DefaultSetup; // existing pro/free users setup without a dialog } else { setupStrategy = await this.showDialog(); } - if (setupStrategy === ChatSetupStrategy.DefaultSetup && this.controller.value.hasEnterpriseProviderConfigured()) { + if (setupStrategy === ChatSetupStrategy.DefaultSetup && ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { setupStrategy = ChatSetupStrategy.SetupWithEnterpriseProvider; // users with a configured provider go through provider setup } @@ -448,7 +629,7 @@ class ChatSetup { success = false; } - return success; + return { success, dialogSkipped }; } private async showDialog(): Promise { @@ -555,34 +736,70 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy): void { - const registration = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload - + const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload + const otherAgentsAndToolsDisposable = markAsSingleton(new MutableDisposable()); const updateRegistration = () => { const disabled = context.state.hidden; - if (!disabled && !registration.value) { - const disposables = registration.value = new DisposableStore(); + if (!disabled) { - // Panel Agents - for (const mode of [ChatMode.Ask, ChatMode.Edit, ChatMode.Agent]) { - const { agent, disposable } = SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Panel, mode, context, controller); - disposables.add(disposable); - disposables.add(agent.onUnresolvableError(() => { - // An unresolvable error from our agent registrations means that - // Copilot is unhealthy for some reason. We clear our panel - // registration to give Copilot a chance to show a custom message - // to the user from the views and stop pretending as if there was - // a functional agent. - this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.'); - disposable.dispose(); - })); + if (!defaultAgentDisposables.value) { + const disposables = defaultAgentDisposables.value = new DisposableStore(); + + // Panel Agents + const panelAgentDisposables = disposables.add(new DisposableStore()); + for (const mode of [ChatMode.Ask, ChatMode.Edit, ChatMode.Agent]) { + const { agent, disposable } = SetupChatAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Panel, mode, context, controller); + panelAgentDisposables.add(disposable); + panelAgentDisposables.add(agent.onUnresolvableError(() => { + // An unresolvable error from our agent registrations means that + // Copilot is unhealthy for some reason. We clear our panel + // registration to give Copilot a chance to show a custom message + // to the user from the views and stop pretending as if there was + // a functional agent. + this.logService.error('[chat setup] Unresolvable error from Copilot agent registration, clearing registration.'); + panelAgentDisposables.dispose(); + })); + } + + // Inline Agents + disposables.add(SetupChatAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable); + disposables.add(SetupChatAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable); + disposables.add(SetupChatAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable); } - // Inline Agents - disposables.add(SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Terminal, undefined, context, controller).disposable); - disposables.add(SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Notebook, undefined, context, controller).disposable); - disposables.add(SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Editor, undefined, context, controller).disposable); - } else if (disabled && registration.value) { - registration.clear(); + if (!context.state.installed && !otherAgentsAndToolsDisposable.value) { + const disposables = otherAgentsAndToolsDisposable.value = new DisposableStore(); + // VSCode Agent + disposables.add(SetupChatAgent.registerOtherAgents(this.instantiationService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Panel, undefined, context, controller).disposable); + + // Tools + disposables.add(SetupTool.registerTools(this.instantiationService, { + id: 'setup.tools.createNewWorkspace', + source: { + type: 'internal', + }, + icon: Codicon.newFolder, + displayName: localize('setupToolDisplayName', "New Workspace"), + modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), + userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), + canBeReferencedInPrompt: true, + toolReferenceName: 'new', + when: ContextKeyExpr.true(), + supportsToolPicker: true, + }).disposable); + } + + } else { + if (defaultAgentDisposables.value) { + defaultAgentDisposables.clear(); + } + if (otherAgentsAndToolsDisposable.value) { + otherAgentsAndToolsDisposable.clear(); + } + } + if (context.state.installed && otherAgentsAndToolsDisposable.value) { + // we need to do this to prevent showing duplicate agent/tool entries in the list + otherAgentsAndToolsDisposable.clear(); } }; @@ -605,16 +822,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr title: CHAT_SETUP_ACTION_LABEL, category: CHAT_CATEGORY, f1: true, - precondition: chatSetupTriggerContext, - menu: { - id: MenuId.ChatTitleBarMenu, - group: 'a_last', - order: 1, - when: ContextKeyExpr.and( - chatSetupTriggerContext, - ChatContextKeys.Setup.hidden - ) - } + precondition: chatSetupTriggerContext }); } @@ -635,8 +843,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } const setup = ChatSetup.getInstance(instantiationService, context, controller); - const result = await setup.run(); - if (result === false && !lifecycleService.willShutdown) { + const { success } = await setup.run(); + if (success === false && !lifecycleService.willShutdown) { const { confirmed } = await dialogService.confirm({ type: Severity.Error, message: localize('setupErrorDialog', "Copilot setup failed. Would you like to try again?"), @@ -650,6 +858,53 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } } + class ChatSetupTriggerWithoutDialogAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.triggerSetupWithoutDialog', + title: CHAT_SETUP_ACTION_LABEL, + precondition: chatSetupTriggerContext + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const layoutService = accessor.get(IWorkbenchLayoutService); + const instantiationService = accessor.get(IInstantiationService); + + await context.update({ hidden: false }); + + const chatWidget = await showCopilotView(viewsService, layoutService); + ChatSetup.getInstance(instantiationService, context, controller).skipDialog(); + chatWidget?.acceptInput(localize('setupCopilot', "Set up Copilot.")); + } + } + + class ChatSetupFromAccountsAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.chat.triggerSetupFromAccounts', + title: localize2('triggerChatSetupFromAccounts', "Sign in to use Copilot..."), + menu: { + id: MenuId.AccountsContext, + group: '2_copilot', + when: ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Entitlement.signedOut + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + return commandService.executeCommand(CHAT_SETUP_ACTION_ID); + } + } + class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; @@ -715,9 +970,12 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr id: MenuId.ChatTitleBarMenu, group: 'a_first', order: 1, - when: ContextKeyExpr.or( - ChatContextKeys.chatQuotaExceeded, - ChatContextKeys.completionsQuotaExceeded + when: ContextKeyExpr.and( + ChatContextKeys.Entitlement.limited, + ContextKeyExpr.or( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.completionsQuotaExceeded + ) ) } }); @@ -751,6 +1009,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } registerAction2(ChatSetupTriggerAction); + registerAction2(ChatSetupFromAccountsAction); + registerAction2(ChatSetupTriggerWithoutDialogAction); registerAction2(ChatSetupHideAction); registerAction2(UpgradePlanAction); } @@ -1004,11 +1264,6 @@ class ChatSetupController extends Disposable { } } - hasEnterpriseProviderConfigured(): boolean { - const setting = this.configurationService.getValue<{ authProvider: unknown } | undefined>(defaultChat.completionsAdvancedSetting); - return setting?.authProvider === defaultChat.enterpriseProviderId; - } - async setupWithProvider(options: { useEnterpriseProvider: boolean }): Promise { const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 5e2760f12f7..f81a703fe5b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -11,7 +11,7 @@ import { localize } from '../../../../nls.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType } from '../../../../base/browser/dom.js'; -import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService } from '../common/chatEntitlementService.js'; +import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService, IQuotaSnapshot } from '../common/chatEntitlementService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; @@ -99,6 +99,7 @@ const defaultChat = { completionsEnablementSetting: product.defaultChatAgent?.completionsEnablementSetting ?? '', nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', + manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '', }; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { @@ -171,7 +172,8 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu let kind: StatusbarEntryKind | undefined; if (!isNewUser(this.chatEntitlementService)) { - const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; + const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; + const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; // Signed out if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { @@ -182,15 +184,15 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu kind = 'prominent'; } - // Quota Exceeded - else if (chatQuotaExceeded || completionsQuotaExceeded) { + // Free Quota Exceeded + else if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited && (chatQuotaExceeded || completionsQuotaExceeded)) { let quotaWarning: string; if (chatQuotaExceeded && !completionsQuotaExceeded) { - quotaWarning = localize('chatQuotaExceededStatus', "Chat limit reached"); + quotaWarning = localize('chatQuotaExceededStatus', "Chat quota reached"); } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - quotaWarning = localize('completionsQuotaExceededStatus', "Completions limit reached"); + quotaWarning = localize('completionsQuotaExceededStatus', "Completions quota reached"); } else { - quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Limit reached"); + quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Quota reached"); } text = `$(copilot-warning) ${quotaWarning}`; @@ -232,9 +234,10 @@ function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { function canUseCopilot(chatEntitlementService: IChatEntitlementService): boolean { const newUser = isNewUser(chatEntitlementService); const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; - const allQuotaReached = chatEntitlementService.quotas.chatQuotaExceeded && chatEntitlementService.quotas.completionsQuotaExceeded; + const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; + const allFreeQuotaReached = limited && chatEntitlementService.quotas.chat?.percentRemaining === 0 && chatEntitlementService.quotas.completions?.percentRemaining === 0; - return !newUser && !signedOut && !allQuotaReached; + return !newUser && !signedOut && !allFreeQuotaReached; } function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { @@ -297,26 +300,30 @@ class ChatStatusDashboard extends Disposable { }; // Quota Indicator - if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate, chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; + const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota, resetDate } = this.chatEntitlementService.quotas; + if (chatQuota || completionsQuota || premiumChatQuota) { - addSeparator(localize('usageTitle', "Copilot Free Plan Usage"), toAction({ - id: 'workbench.action.openChatSettings', + addSeparator(localize('usageTitle', "Copilot Usage"), toAction({ + id: 'workbench.action.manageCopilot', label: localize('quotaLabel', "Manage Copilot"), tooltip: localize('quotaTooltip', "Manage Copilot"), class: ThemeIcon.asClassName(Codicon.settings), run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))), })); - const chatQuotaIndicator = this.createQuotaIndicator(this.element, chatTotal, chatRemaining, localize('chatsLabel', "Chat messages")); - const completionsQuotaIndicator = this.createQuotaIndicator(this.element, completionsTotal, completionsRemaining, localize('completionsLabel', "Code completions")); + const completionsQuotaIndicator = completionsQuota ? this.createQuotaIndicator(this.element, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; + const chatQuotaIndicator = chatQuota ? this.createQuotaIndicator(this.element, chatQuota, premiumChatQuota ? localize('basicChatsLabel', "Basic chat requests") : localize('chatsLabel', "Chat requests"), false) : undefined; + const premiumChatQuotaIndicator = premiumChatQuota ? this.createQuotaIndicator(this.element, premiumChatQuota, localize('premiumChatsLabel', "Premium chat requests"), true) : undefined; - this.element.appendChild($('div.description', undefined, localize('limitQuota', "Limits will reset on {0}.", this.dateFormatter.value.format(quotaResetDate)))); + if (resetDate) { + this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance renews on {0}.", this.dateFormatter.value.format(new Date(resetDate))))); + } - if (chatQuotaExceeded || completionsQuotaExceeded) { - const upgradePlanButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); - upgradePlanButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); - disposables.add(upgradePlanButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan'))); + const limited = this.chatEntitlementService.entitlement === ChatEntitlement.Limited; + if ((limited && (chatQuota?.percentRemaining === 0 || completionsQuota?.percentRemaining === 0)) || (!limited && premiumChatQuota?.percentRemaining === 0 && !premiumChatQuota.overageEnabled)) { + const button = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); + button.label = limited ? localize('upgradeToCopilotPro', "Upgrade to Copilot Pro") : localize('enableAdditionalUsage', "Enable Pay for Additional Requests"); + disposables.add(button.onDidClick(() => this.runCommandAndClose(limited ? 'workbench.action.chat.upgradePlan' : () => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); } (async () => { @@ -325,10 +332,16 @@ class ChatStatusDashboard extends Disposable { return; } - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = this.chatEntitlementService.quotas; - - chatQuotaIndicator(chatTotal, chatRemaining); - completionsQuotaIndicator(completionsTotal, completionsRemaining); + const { chat: chatQuota, completions: completionsQuota, premiumChat: premiumChatQuota } = this.chatEntitlementService.quotas; + if (completionsQuota) { + completionsQuotaIndicator?.(completionsQuota); + } + if (chatQuota) { + chatQuotaIndicator?.(chatQuota); + } + if (premiumChatQuota) { + premiumChatQuotaIndicator?.(premiumChatQuota); + } })(); } @@ -446,42 +459,60 @@ class ChatStatusDashboard extends Disposable { this.hoverService.hideHover(true); } - private createQuotaIndicator(container: HTMLElement, total: number | undefined, remaining: number | undefined, label: string): (total: number | undefined, remaining: number | undefined) => void { - const quotaText = $('span.quota-percentage'); + private createQuotaIndicator(container: HTMLElement, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { + const quotaValue = $('span.quota-value'); const quotaBit = $('div.quota-bit'); + const overageLabel = $('span.overage-label'); const quotaIndicator = container.appendChild($('div.quota-indicator', undefined, $('div.quota-label', undefined, $('span', undefined, label), - quotaText + quotaValue ), $('div.quota-bar', undefined, quotaBit + ), + $('div.overage', undefined, + overageLabel ) )); - const update = (total: number | undefined, remaining: number | undefined) => { + const update = (quota: IQuotaSnapshot) => { quotaIndicator.classList.remove('error'); quotaIndicator.classList.remove('warning'); + quotaIndicator.classList.remove('unlimited'); - if (typeof total === 'number' && typeof remaining === 'number') { - let usedPercentage = Math.round(((total - remaining) / total) * 100); - if (total !== remaining && usedPercentage === 0) { + if (quota.unlimited) { + quotaIndicator.classList.add('unlimited'); + quotaValue.textContent = localize('quotaUnlimited', "Included"); + } else { + let usedPercentage = Math.max(0, 100 - quota.percentRemaining); + if (usedPercentage === 0) { usedPercentage = 1; // indicate minimal usage as 1% } - quotaText.textContent = localize('quotaDisplay', "{0}%", usedPercentage); + quotaValue.textContent = quota.overageCount ? localize('quotaDisplayWithOverage', "{0}% +{1}", usedPercentage, quota.overageCount) : localize('quotaDisplay', "{0}%", usedPercentage); quotaBit.style.width = `${usedPercentage}%`; - if (usedPercentage >= 90) { + if (usedPercentage >= 90 && !quota.overageEnabled) { quotaIndicator.classList.add('error'); } else if (usedPercentage >= 75) { quotaIndicator.classList.add('warning'); } } + + if (supportsOverage) { + if (quota.overageEnabled) { + overageLabel.textContent = localize('additionalUsageEnabled', "Pay per additional requests is enabled."); + } else { + overageLabel.textContent = localize('additionalUsageDisabled', "Pay per additional requests is disabled."); + } + } else { + overageLabel.textContent = ''; + } }; - update(total, remaining); + update(quota); return update; } @@ -504,13 +535,13 @@ class ChatStatusDashboard extends Disposable { // --- Next Edit Suggestions { const setting = append(settings, $('div.setting')); - this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next Edit Suggestions"), modeId, this.getCompletionsSettingAccessor(modeId), disposables); + this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next Edit Suggestions"), this.getCompletionsSettingAccessor(modeId), disposables); } return settings; } - private createSetting(container: HTMLElement, settingId: string, label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox { + private createSetting(container: HTMLElement, settingIdsToReEvaluate: string[], label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox { const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), defaultCheckboxStyles)); container.appendChild(checkbox.domNode); @@ -533,7 +564,7 @@ class ChatStatusDashboard extends Disposable { })); disposables.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(settingId)) { + if (settingIdsToReEvaluate.some(id => e.affectsConfiguration(id))) { checkbox.checked = Boolean(accessor.readSetting()); } })); @@ -541,13 +572,14 @@ class ChatStatusDashboard extends Disposable { if (!canUseCopilot(this.chatEntitlementService)) { container.classList.add('disabled'); checkbox.disable(); + checkbox.checked = false; } return checkbox; } private createCodeCompletionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void { - this.createSetting(container, defaultChat.completionsEnablementSetting, label, this.getCompletionsSettingAccessor(modeId), disposables); + this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId), disposables); } private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor { @@ -566,13 +598,13 @@ class ChatStatusDashboard extends Disposable { }; } - private createNextEditSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void { + private createNextEditSuggestionsSetting(container: HTMLElement, label: string, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void { const nesSettingId = defaultChat.nextEditSuggestionsSetting; const completionsSettingId = defaultChat.completionsEnablementSetting; const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - const checkbox = this.createSetting(container, nesSettingId, label, { - readSetting: () => this.textResourceConfigurationService.getValue(resource, nesSettingId), + const checkbox = this.createSetting(container, [nesSettingId, completionsSettingId], label, { + readSetting: () => completionsSettingAccessor.readSetting() && this.textResourceConfigurationService.getValue(resource, nesSettingId), writeSetting: (value: boolean) => this.textResourceConfigurationService.updateValue(resource, nesSettingId, value) }, disposables); diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 2887ef3c14b..573f54f0aad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../base/common/arrays.js'; -import { IChatRequestVariableData, IChatRequestVariableEntry } from '../common/chatModel.js'; -import { ChatRequestDynamicVariablePart, ChatRequestToolPart, IParsedChatRequest } from '../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js'; +import { IToolData } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js'; @@ -17,33 +15,6 @@ export class ChatVariablesService implements IChatVariablesService { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { } - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData { - let resolvedVariables: IChatRequestVariableEntry[] = []; - - prompt.parts - .forEach((part, i) => { - if (part instanceof ChatRequestDynamicVariablePart || part instanceof ChatRequestToolPart) { - resolvedVariables[i] = part.toVariableEntry(); - } - }); - - // Make array not sparse - resolvedVariables = coalesce(resolvedVariables); - - // "reverse", high index first so that replacement is simple - resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); - - if (attachedContextVariables) { - // attachments not in the prompt - resolvedVariables.push(...attachedContextVariables); - } - - - return { - variables: resolvedVariables, - }; - } - getDynamicVariables(sessionId: string): ReadonlyArray { // This is slightly wrong... the parser pulls dynamic references from the input widget, but there is no guarantee that message came from the input here. // Need to ... @@ -62,4 +33,12 @@ export class ChatVariablesService implements IChatVariablesService { return model.variables; } + getSelectedTools(sessionId: string): ReadonlyArray { + const widget = this.chatWidgetService.getWidgetBySessionId(sessionId); + if (!widget) { + return []; + } + return widget.input.selectedToolsModel.tools.get(); + } + } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 63e8a5dc98d..cf16001e255 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -100,6 +100,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const info = this.getTransferredOrPersistedSessionInfo(); this._restoringSession = (info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : Promise.resolve(undefined)).then(async model => { + if (!this._widget) { + // renderBody has not been called yet + return; + } + // The widget may be hidden at this point, because welcome views were allowed. Use setVisible to // avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome` // so it should fire onDidChangeViewWelcomeState. diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 1b57c1de5d6..40d3f741b27 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -6,6 +6,8 @@ import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; +import { pick } from '../../../../base/common/arrays.js'; +import { assert } from '../../../../base/common/assert.js'; import { disposableTimeout, timeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; @@ -23,6 +25,7 @@ import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -40,8 +43,8 @@ import { checkModeOption } from '../common/chat.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js'; -import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestToolPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; +import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel, IPromptVariableEntry } from '../common/chatModel.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; import { IChatSlashCommandService } from '../common/chatSlashCommands.js'; @@ -49,9 +52,13 @@ import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from import { IChatInputState } from '../common/chatWidgetHistoryService.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; import { ChatAgentLocation, ChatMode } from '../common/constants.js'; +import { FilePromptParser } from '../common/promptSyntax/parsers/filePromptParser.js'; +import { IPromptsService } from '../common/promptSyntax/service/types.js'; +import { IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; +import { isPromptFileChatVariable, toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; import { ChatInputPart, IChatInputStyles } from './chatInputPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; @@ -241,6 +248,8 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatEditingService chatEditingService: IChatEditingService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IPromptsService private readonly promptsService: IPromptsService, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -501,12 +510,11 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(autorun(r => { const input = parsedInput.read(r); - const newAttachments = new Map(); + const newPromptAttachments = new Map(); const oldPromptAttachments = new Set(); // get all attachments, know those that are prompt-referenced for (const attachment of this.attachmentModel.attachments) { - newAttachments.set(attachment.id, attachment); if (attachment.range) { oldPromptAttachments.add(attachment.id); } @@ -516,17 +524,12 @@ export class ChatWidget extends Disposable implements IChatWidget { for (const part of input.parts) { if (part instanceof ChatRequestToolPart || part instanceof ChatRequestDynamicVariablePart) { const entry = part.toVariableEntry(); - newAttachments.set(entry.id, entry); + newPromptAttachments.set(entry.id, entry); oldPromptAttachments.delete(entry.id); } } - // delete old prompt-referenced attachments - for (const id of oldPromptAttachments) { - newAttachments.delete(id); - } - - this.attachmentModel.clearAndSetContext(...newAttachments.values()); + this.attachmentModel.updateContent(oldPromptAttachments, newPromptAttachments.values()); })); } @@ -776,7 +779,6 @@ export class ChatWidget extends Disposable implements IChatWidget { attempt: request.attempt + 1, location: this.location, userSelectedModelId: this.input.currentLanguageModel, - hasInstructionAttachments: this.input.hasInstructionAttachments, mode: this.input.currentMode, }; this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e)); @@ -970,7 +972,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); - this._register(this.inputPart.attachmentModel.onDidChangeContext(() => { + this._register(this.inputPart.attachmentModel.onDidChange(() => { if (this._editingSession) { // TODO still needed? Do this inside input part and fire onDidChangeHeight? this.renderChatEditingSessionState(); @@ -991,6 +993,10 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderWelcomeViewContentIfNeeded(); this.refreshParsedInput(); })); + this._register(autorun(r => { + this.input.selectedToolsModel.tools.read(r); // SIGNAL + this.refreshParsedInput(); + })); } private onDidStyleChange(): void { @@ -1141,6 +1147,23 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } + private async _handlePromptSlashCommand(): Promise<{ input?: string; attachment?: IChatRequestVariableEntry }> { + + const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart); + if (!agentSlashPromptPart) { + return {}; + } + + const promptPath = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand); + if (!promptPath) { + return {}; + } + + const attachment = toChatVariable({ uri: promptPath.uri, isPromptFile: true }, true); + const input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); + return { attachment, input }; + } + private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise { if (this.viewModel?.requestInProgress && this.viewModel.requestPausibility !== ChatPauseState.Paused) { return; @@ -1152,22 +1175,57 @@ export class ChatWidget extends Disposable implements IChatWidget { const editorValue = this.getInput(); const requestId = this.chatAccessibilityService.acceptRequest(); - const input = !query ? editorValue : query.query; + let input = !query ? editorValue : query.query; const isUserQuery = !query; const { promptInstructions } = this.inputPart.attachmentModel; const instructionsEnabled = promptInstructions.featureEnabled; if (instructionsEnabled) { - // instruction files may have nested child references to other prompt - // files that are resolved asynchronously, hence we need to wait for - // the entire prompt instruction tree to be processed + // prompt files may have nested child references to other prompt + // files that are resolved asynchronously, hence we need to wait + // for the entire prompt instruction tree to be processed const instructionsStarted = performance.now(); await promptInstructions.allSettled(); // allow-any-unicode-next-line this.logService.trace(`[⏱] instructions tree resolved in ${performance.now() - instructionsStarted}ms`); } - let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); + let attachedContext = await this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); + if (instructionsEnabled) { + const result = await this._handlePromptSlashCommand(); + if (result.input !== undefined) { + input = result.input; + } + + if (result.attachment) { + attachedContext.push(result.attachment); + } + + const promptFileVariables = attachedContext.filter(isPromptFileChatVariable); + if (promptFileVariables.length > 0) { + input = `Follow the prompt instructions from ${promptFileVariables.map(v => v.name).join(', ')}\n${input}`; + + const allToolsMetadata = await this.getPromptFileToolsMetadata(promptFileVariables); + + // if there are some tools defined in the prompt files, switch to + // the agent mode and select only the specified tools + if ((allToolsMetadata !== null) && (allToolsMetadata.length > 0)) { + const options: IToggleChatModeArgs = { mode: ChatMode.Agent }; + + // tools are currently only available in agent mode hence + // switch to the mode before updating the selected tools + await this.commandService.executeCommand( + ToggleAgentModeActionId, options, + ); + + // update the selected tools + this.inputPart + .selectedToolsModel + .selectOnly(allToolsMetadata); + } + } + } + if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) { const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext; @@ -1211,7 +1269,6 @@ export class ChatWidget extends Disposable implements IChatWidget { parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.inputPart.currentMode }, attachedContext, noCommandDetection: options?.noCommandDetection, - hasInstructionAttachments: this.inputPart.hasInstructionAttachments, userSelectedTools: this.input.currentMode === ChatMode.Agent ? this.inputPart.selectedToolsModel.tools.get().map(tool => tool.id) : undefined }); @@ -1425,6 +1482,89 @@ export class ChatWidget extends Disposable implements IChatWidget { const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart); this.agentInInput.set(!!currentAgent); } + + /** + * Gets a list of all tools specified in the provided prompt files. + */ + private async getPromptFileToolsMetadata( + variables: readonly IPromptVariableEntry[], + ): Promise { + // process starting from the 'root' prompt files + const rootVariables = variables + .filter(pick('isRoot')); + + if (rootVariables.length === 0) { + return null; + } + + // create tasks to parse all the prompt files + // and wait for all the precesses to finish + const toolMetadataPromises = rootVariables + .map((variable) => { + const { value } = variable; + + const uri = URI.isUri(value) + ? value + : value.uri; + + assert( + URI.isUri(uri), + `Prompt file variable must have a URI value, got '${uri}'.`, + ); + + const prompt = this.instantiationService.createInstance( + FilePromptParser, + uri, + { allowNonPromptFiles: true }, + ) + .start() + .allSettled(); + + return prompt; + }); + + const allToolsMetadata = (await Promise.allSettled(toolMetadataPromises)) + .filter((result): result is PromiseFulfilledResult => { + const { status } = result; + const isFulfilled = (status === 'fulfilled'); + + if (isFulfilled === false) { + const { reason } = result; + + this.logService.error( + 'failed to parse prompt file', reason, + ); + } + + return isFulfilled; + }) + .map(({ value: prompt }) => { + return prompt.allToolsMetadata; + }); + + // flag to track whether any of the prompt files + // contained any tools metadata we can use + let hasMetadata = false; + const result: string[] = []; + + // copy over all the tools metadata into single array + // keep tracking if any of them contained any metadata + for (const maybeMetadata of allToolsMetadata) { + if (maybeMetadata === null) { + continue; + } + + hasMetadata = true; + result.push(...maybeMetadata); + } + + // if no prompt files contained tools metadata, return null + if (hasMetadata === false) { + return null; + } + + return result; + } } export class ChatWidgetService extends Disposable implements IChatWidgetService { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 98d96742d47..f601d70bedb 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, groupBy } from '../../../../../base/common/arrays.js'; -import { assertNever } from '../../../../../base/common/assert.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; @@ -18,22 +17,17 @@ import { URI } from '../../../../../base/common/uri.js'; import { IRange, Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { Command, isLocation } from '../../../../../editor/common/languages.js'; -import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { FileType, IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { PromptsConfig } from '../../../../../platform/prompts/common/config.js'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; -import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; -import { IChatRequestProblemsVariable, IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; +import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; @@ -184,11 +178,10 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC private updateDecorations(): void { - const decorations = this._variables.map((r): IDecorationOptions => ({ + const decorationIds = this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({ range: r.range, hoverMessage: this.getHoverForReference(r) - })); - const decorationIds = this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, decorations); + }))); this.decorationData = []; for (let i = 0; i < decorationIds.length; i++) { @@ -241,15 +234,6 @@ function isDynamicVariable(obj: any): obj is IDynamicVariable { ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); -interface SelectAndInsertActionContext { - widget: IChatWidget; - range: IRange; -} - -function isSelectAndInsertActionContext(context: any): context is SelectAndInsertActionContext { - return 'widget' in context && 'range' in context; -} - export async function createFolderQuickPick(accessor: ServicesAccessor): Promise { const quickInputService = accessor.get(IQuickInputService); @@ -511,132 +495,3 @@ export class AddDynamicVariableAction extends Action2 { } } registerAction2(AddDynamicVariableAction); - -export async function createMarkersQuickPick(accessor: ServicesAccessor, level: 'problem' | 'file', onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { - const markers = accessor.get(IMarkerService).read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); - if (!markers.length) { - return; - } - - const uriIdentityService = accessor.get(IUriIdentityService); - const labelService = accessor.get(ILabelService); - const grouped = groupBy(markers, (a, b) => uriIdentityService.extUri.compare(a.resource, b.resource)); - - const severities = new Set(); - type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; - const items: (MarkerPickItem | IQuickPickSeparator)[] = []; - - let pickCount = 0; - for (const group of grouped) { - const resource = group[0].resource; - if (level === 'problem') { - items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); - for (const marker of group) { - pickCount++; - severities.add(marker.severity); - items.push({ - type: 'item', - resource: marker.resource, - label: marker.message, - description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), - entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), - }); - } - } else if (level === 'file') { - const entry = { filterUri: resource }; - pickCount++; - items.push({ - type: 'item', - resource, - label: IDiagnosticVariableEntryFilterData.label(entry), - description: group[0].message + (group.length > 1 ? localize('problemsMore', '+ {0} more', group.length - 1) : ''), - entry, - }); - for (const marker of group) { - severities.add(marker.severity); - } - } else { - assertNever(level); - } - } - - if (pickCount < 2) { // single error in a URI - return items.find((i): i is MarkerPickItem => i.type === 'item')?.entry; - } - - if (level === 'file') { - items.unshift({ type: 'separator', label: localize('markers.panel.files', 'Files') }); - } - - items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); - - const quickInputService = accessor.get(IQuickInputService); - const store = new DisposableStore(); - const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); - quickPick.canAcceptInBackground = !onBackgroundAccept; - quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); - quickPick.items = items; - - return new Promise(resolve => { - store.add(quickPick.onDidHide(() => resolve(undefined))); - store.add(quickPick.onDidAccept(ev => { - if (ev.inBackground) { - onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); - } else { - resolve(quickPick.selectedItems[0]?.entry); - quickPick.dispose(); - } - })); - quickPick.show(); - }).finally(() => store.dispose()); -} - -export class SelectAndInsertProblemAction extends Action2 { - static readonly Name = 'problems'; - static readonly ID = 'workbench.action.chat.selectAndInsertProblems'; - - constructor() { - super({ - id: SelectAndInsertProblemAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const logService = accessor.get(ILogService); - const context = args[0]; - if (!isSelectAndInsertActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `problem` - context.widget.inputEditor.executeEdits('chatInsertProblems', [{ range: context.range, text: `` }]); - }; - - const pick = await createMarkersQuickPick(accessor, 'file'); - if (!pick) { - doCleanup(); - return; - } - - const editor = context.widget.inputEditor; - const originalRange = context.range; - const insertText = `#${SelectAndInsertProblemAction.Name}:${pick.filterUri ? basename(pick.filterUri) : MarkerSeverity.toString(pick.filterSeverity!)}`; - - const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length); - const success = editor.executeEdits('chatInsertProblems', [{ range: varRange, text: insertText + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertProblemsAction: failed to insert "${insertText}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.problems', - range: varRange, - data: { id: 'vscode.problems', filter: pick } satisfies IChatRequestProblemsVariable, - }); - } -} -registerAction2(SelectAndInsertProblemAction); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index 5a6d6a5ef57..20da77d613a 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -224,11 +224,11 @@ export class ChatImplicitContextContribution extends Disposable implements IWork const setting = this._implicitContextEnablement[widget.location]; const isFirstInteraction = widget.viewModel?.getItems().length === 0; if (setting === 'first' && !isFirstInteraction) { - widget.input.implicitContext.setValue(undefined, false, languageId); + widget.input.implicitContext.setValue(undefined, false, undefined); } else if (setting === 'always' || setting === 'first' && isFirstInteraction) { widget.input.implicitContext.setValue(newValue, isSelection, languageId); } else if (setting === 'never') { - widget.input.implicitContext.setValue(undefined, false, languageId); + widget.input.implicitContext.setValue(undefined, false, undefined); } } } @@ -238,7 +238,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli get id() { // IDs for prompt files need to start with a special prefix // that is used by the copilot extension to identify them - if (this.isPrompt) { + if (this.isPromptFile) { assertDefined( this.value, 'Implicit prompt attachments must have a value.', @@ -265,7 +265,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } get name(): string { - const fileType = this.isPrompt ? 'prompt' : 'file'; + const fileType = this.isPromptFile ? 'prompt' : 'file'; if (URI.isUri(this.value)) { return `${fileType}:${basename(this.value)}`; @@ -279,13 +279,13 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli readonly kind = 'implicit'; get modelDescription(): string { - if (this.isPrompt) { + if (this.isPromptFile) { if (URI.isUri(this.value)) { - return `User's active prompt instructions file`; + return `User's active prompt file`; } else if (this._isSelection) { - return `User's active selection inside prompt instructions`; + return `User's active selection inside prompt file`; } else { - return `User's current visible prompt instructions text`; + return `User's current visible prompt text`; } } @@ -305,7 +305,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli return this._isSelection; } - private _onDidChangeValue = new Emitter(); + private _onDidChangeValue = this._register(new Emitter()); readonly onDidChangeValue = this._onDidChangeValue.event; private _value: Location | URI | undefined; @@ -314,7 +314,7 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli } private _languageId: string | undefined; - get isPrompt() { + get isPromptFile() { return (this._languageId === PROMPT_LANGUAGE_ID); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index de6ac01eda8..41da2f77280 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -27,7 +27,6 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; @@ -38,21 +37,22 @@ import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js import { ISearchService } from '../../../../services/search/common/search.js'; import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IDynamicVariable } from '../../common/chatVariables.js'; import { ChatAgentLocation, ChatMode } from '../../common/constants.js'; -import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; +import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, SelectAndInsertProblemAction, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, + @IPromptsService private readonly promptsService: IPromptsService, ) { super(); @@ -144,6 +144,53 @@ class SlashCommandCompletions extends Disposable { }; } })); + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'promptSlashCommands', + triggerCharacters: ['/'], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || !widget.viewModel) { + return null; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + if (!isEmptyUpToCompletionWord(model, range)) { + // No text allowed before the completion + return; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + + const promptCommands = await this.promptsService.findPromptSlashCommands(); + if (promptCommands.length === 0) { + return null; + } + + return { + suggestions: promptCommands.map((c, i): CompletionItem => { + const label = `/${c.command}`; + const description = c.promptPath?.storage === 'user' ? localize('promptFileDescription', 'User Prompt File') : localize('promptFileDescriptionWorkspace', 'Workspace Prompt File'); + return { + label: { label, description }, + insertText: `${label} `, + documentation: c.detail, + range, + sortText: 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + }; + }) + }; + } + })); } } @@ -179,8 +226,8 @@ class AgentCompletions extends Disposable { return; } - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { + const usedOtherCommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashPromptPart); + if (usedOtherCommand) { // Only one allowed return; } @@ -452,7 +499,7 @@ interface IVariableCompletionsDetails { class BuiltinDynamicCompletions extends Disposable { private static readonly addReferenceCommand = '_addReferenceCmd'; - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:]*`, 'g'); // MUST be using `g`-flag + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}[\\w:-]*`, 'g'); // MUST be using `g`-flag private readonly queryBuilder: QueryBuilder; @@ -469,7 +516,6 @@ class BuiltinDynamicCompletions extends Disposable { @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, - @IMarkerService markerService: IMarkerService, ) { super(); @@ -564,30 +610,6 @@ class BuiltinDynamicCompletions extends Disposable { return result; }); - // Problems completions, we just attach all problems in this case - this.registerVariableCompletions(SelectAndInsertProblemAction.Name, ({ widget, range, position, model }, token) => { - const stats = markerService.getStatistics(); - if (!stats.errors && !stats.warnings) { - return null; - } - - const result: CompletionList = { suggestions: [] }; - - const completedText = `${chatVariableLeader}${SelectAndInsertProblemAction.Name}:`; - const afterTextRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + completedText.length); - result.suggestions.push({ - label: `${chatVariableLeader}${SelectAndInsertProblemAction.Name}`, - insertText: completedText, - documentation: localize('pickProblemsLabel', "Problems in your workspace"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertProblemAction.ID, title: SelectAndInsertProblemAction.ID, arguments: [{ widget, range: afterTextRange }] }, - sortText: 'z' - }); - - return result; - }); - this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); @@ -911,7 +933,6 @@ class ToolCompletions extends Disposable { constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @ILanguageModelToolsService toolsService: ILanguageModelToolsService ) { super(); @@ -932,14 +953,22 @@ class ToolCompletions extends Disposable { const usedTools = widget.parsedInput.parts.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart); const usedToolNames = new Set(usedTools.map(v => v.toolName)); const toolItems: CompletionItem[] = []; - toolItems.push(...Array.from(toolsService.getTools()) + toolItems.push(...widget.input.selectedToolsModel.tools.get() .filter(t => t.canBeReferencedInPrompt) .filter(t => !usedToolNames.has(t.toolReferenceName ?? '')) .map((t): CompletionItem => { + const source = t.source; + const detail = source.type === 'mcp' + ? localize('desc', "MCP Server: {0}", source.label) + : source.type === 'extension' + ? source.label + : undefined; + const withLeader = `${chatVariableLeader}${t.toolReferenceName}`; return { label: withLeader, range, + detail, insertText: withLeader + ' ', documentation: t.userDescription, kind: CompletionItemKind.Text, diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 1d0953b5e26..3f2bb1b4b84 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -15,7 +15,7 @@ import { inputPlaceholderForeground } from '../../../../../platform/theme/common import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../common/chatColors.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget } from '../chatWidget.js'; @@ -142,6 +142,7 @@ class InputEditorDecorations extends Disposable { const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); const slashCommandPart = parsedRequest.find((p): p is ChatRequestSlashCommandPart => p instanceof ChatRequestSlashCommandPart); + const slashPromptPart = parsedRequest.find((p): p is ChatRequestSlashPromptPart => p instanceof ChatRequestSlashPromptPart); const exactlyOneSpaceAfterPart = (part: IParsedChatRequestPart): boolean => { const partIdx = parsedRequest.indexOf(part); @@ -226,6 +227,10 @@ class InputEditorDecorations extends Disposable { textDecorations.push({ range: slashCommandPart.editorRange }); } + if (slashPromptPart) { + textDecorations.push({ range: slashPromptPart.editorRange }); + } + this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); const varDecorations: IDecorationOptions[] = []; @@ -307,7 +312,7 @@ class ChatTokenDeleter extends Disposable { const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentMode }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping - const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestToolPart); + const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestSlashPromptPart || p instanceof ChatRequestToolPart); deletableTokens.forEach(token => { const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range); // Part of this token was deleted, or the space after it was deleted, and the deletion range doesn't go off the front of the token, for simpler math diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 44c4d6d8946..5b1f9bd553c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -98,7 +98,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben disposableStore.add(onDebouncedType(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); - disposableStore.add(widget.attachmentModel.onDidChangeContext(() => { + disposableStore.add(widget.attachmentModel.onDidChange(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); disposableStore.add(currentEditingSession.onDidDispose(() => { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 63f153f33f3..fd1b3fb7beb 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -25,7 +25,7 @@ -webkit-user-select: text; } -.interactive-item-container .header { +.interactive-item-container:not(:has(.chat-extensions-content-part)) .header { display: flex; align-items: center; justify-content: space-between; @@ -201,7 +201,7 @@ margin-bottom: 8px; } -.interactive-item-container .value .rendered-markdown { +.interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) { .codicon { font-size: inherit; } @@ -241,6 +241,10 @@ color: var(--vscode-textLink-foreground); } +.interactive-item-container .value .rendered-markdown .chat-extensions-content-part a { + color: inherit; +} + .interactive-item-container .value .rendered-markdown a { user-select: text; } @@ -313,13 +317,13 @@ font-weight: unset; } -.interactive-item-container .value .rendered-markdown { - /* Codicons next to text need to be aligned with the text */ - .codicon { - position: relative; - top: 2px; - } +/* Codicons next to text need to be aligned with the text */ +.interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) .codicon { + position: relative; + top: 2px; +} +.interactive-item-container .value .rendered-markdown { .chat-codeblock-pill-widget .codicon { top: -1px; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index a83c5ec9a1c..55f193f87ef 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -54,10 +54,11 @@ .chat-status-bar-entry-tooltip .quota-indicator .quota-label { display: flex; justify-content: space-between; + gap: 20px; margin-bottom: 3px; } -.chat-status-bar-entry-tooltip .quota-indicator .quota-label .quota-percentage { +.chat-status-bar-entry-tooltip .quota-indicator .quota-label .quota-value { color: var(--vscode-descriptionForeground); } @@ -67,6 +68,11 @@ background-color: var(--vscode-gauge-foreground); border-radius: 4px; border: 1px solid var(--vscode-gauge-border); + margin: 4px 0; +} + +.chat-status-bar-entry-tooltip .quota-indicator.unlimited .quota-bar { + display: none; } .chat-status-bar-entry-tooltip .quota-indicator .quota-bar .quota-bit { diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts new file mode 100644 index 00000000000..25b0c02279c --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from '../../../../../base/common/actions.js'; +import { Event } from '../../../../../base/common/event.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; +import { localize } from '../../../../../nls.js'; +import { ModelPickerWidget } from './modelPickerWidget.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import * as dom from '../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { IAnchor } from '../../../../../base/browser/ui/contextview/contextview.js'; + +export interface IModelPickerDelegate { + readonly onDidChangeModel: Event; + setModel(model: ILanguageModelChatMetadataAndIdentifier): void; + getModels(): ILanguageModelChatMetadataAndIdentifier[]; +} + +/** + * Action view item for selecting a language model in the chat interface. + */ +export class ModelPickerActionItem extends ActionViewItem { + private widget: ModelPickerWidget | undefined; + + constructor( + action: IAction, + private readonly currentModel: ILanguageModelChatMetadataAndIdentifier, + private readonly delegate: IModelPickerDelegate, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + // Modify the original action with a different label and make it show the current model + const actionWithLabel: IAction = { + ...action, + label: currentModel.metadata.name, + tooltip: localize('chat.modelPicker.label', "Pick Model"), + run: () => { /* Will be overridden by our click handler */ } + }; + + super(undefined, actionWithLabel, { label: true }); + + // Listen for model changes from the delegate + this._register(delegate.onDidChangeModel(model => { + this.action.label = model.metadata.name; + this.updateLabel(); + })); + } + + /** + * Override rendering of the label to include the dropdown indicator + */ + protected override updateLabel(): void { + if (this.label) { + // Reset the label element with the current model name and a dropdown indicator + dom.reset(this.label, + dom.$('span.chat-model-label', undefined, this.action.label), + ...renderLabelWithIcons(`$(${Codicon.chevronDown.id})`) + ); + } + } + + /** + * Override rendering to add CSS classes and initialize the widget + */ + override render(container: HTMLElement): void { + super.render(container); + + // Add classes for styling this element + container.classList.add('chat-modelPicker-item'); + + // Create the model picker widget that will be shown when clicked + this.widget = this.instantiationService.createInstance( + ModelPickerWidget, + this.currentModel, + () => this.delegate.getModels(), + (model) => this.delegate.setModel(model) + ); + + // Register event handlers + this._register(this.widget.onDidChangeModel(model => { + this.action.label = model.metadata.name; + this.updateLabel(); + })); + } + + /** + * Override the onClick to show our picker widget + */ + override onClick(event: MouseEvent): void { + if (!this.widget || !this.element) { + return; + } + + this.show(this.element); + + event.stopPropagation(); + event.preventDefault(); + } + + show(anchor?: HTMLElement | StandardMouseEvent | IAnchor): void { + if (!this.widget || !this.element) { + return; + } + + // Show the model picker at the current position + this.widget.showAt(anchor ?? this.element); + } + + /** + * Set aria label attributes on the element + */ + protected setAriaLabelAttributes(element: HTMLElement): void { + element.setAttribute('aria-label', localize('chatModelPicker', "Chat Model: {0}", this.action.label)); + element.setAttribute('aria-haspopup', 'true'); + element.setAttribute('role', 'button'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts new file mode 100644 index 00000000000..3bec3dc0419 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerWidget.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from '../../../../../base/common/actions.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IAnchor } from '../../../../../base/browser/ui/contextview/contextview.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../common/languageModels.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { localize } from '../../../../../nls.js'; +import { MenuId, IMenuService } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IChatEntitlementService, ChatEntitlement } from '../../common/chatEntitlementService.js'; +import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ActionListItemKind, IActionListItem } from '../../../../../platform/actionWidget/browser/actionList.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; + +interface IModelPickerActionItem { + model: ILanguageModelChatMetadataAndIdentifier; + isCurrent: boolean; +} + +/** + * Widget for picking a language model for chat. + */ +export class ModelPickerWidget extends Disposable { + private readonly _onDidChangeModel = this._register(new Emitter()); + readonly onDidChangeModel = this._onDidChangeModel.event; + + constructor( + private currentModel: ILanguageModelChatMetadataAndIdentifier, + private readonly getModels: () => ILanguageModelChatMetadataAndIdentifier[], + private readonly setModel: (model: ILanguageModelChatMetadataAndIdentifier) => void, + @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, + ) { + super(); + } + + /** + * Get the label to display in the button that shows the current model + */ + get buttonLabel(): string { + return this.currentModel.metadata.name; + } + + /** + * Convert available models to action items for display + */ + private getActionItems(): IModelPickerActionItem[] { + const items: IModelPickerActionItem[] = this.getModels().map(model => ({ + model, + isCurrent: model.identifier === this.currentModel.identifier + })); + + return items; + } + + /** + * Get any additional actions to add to the picker menu + */ + private getAdditionalActions(): IAction[] { + const menuActions = this.menuService.createMenu(MenuId.ChatModelPicker, this.contextKeyService); + const menuContributions = getFlatActionBarActions(menuActions.getActions()); + menuActions.dispose(); + + const additionalActions: IAction[] = []; + + // Add menu contributions from extensions + if (menuContributions.length > 0) { + additionalActions.push(...menuContributions); + } + + // Add upgrade option if entitlement is limited + if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { + additionalActions.push({ + id: 'moreModels', + label: localize('chat.moreModels', "Add Premium Models"), + enabled: true, + tooltip: localize('chat.moreModels.tooltip', "Add premium models"), + class: undefined, + run: () => { + const commandId = 'workbench.action.chat.upgradePlan'; + this.commandService.executeCommand(commandId); + } + }); + } + + return additionalActions; + } + + /** + * Shows the picker at the specified anchor + */ + showAt(anchor: HTMLElement | StandardMouseEvent | IAnchor, container?: HTMLElement): void { + const items: IActionListItem[] = this.getActionItems().map(item => ({ + item: item.model, + description: item.model.metadata.description, + kind: ActionListItemKind.Action, + canPreview: false, + group: { title: '', icon: ThemeIcon.fromId(item.isCurrent ? Codicon.check.id : Codicon.blank.id) }, + disabled: false, + hideIcon: false, + label: item.model.metadata.name, + } satisfies IActionListItem)); + + const delegate = { + onSelect: (item: ILanguageModelChatMetadataAndIdentifier) => { + if (item.identifier !== this.currentModel.identifier) { + this.setModel(item); + this.currentModel = item; + this._onDidChangeModel.fire(item); + } + this.actionWidgetService.hide(false); + return true; + }, + onHide: () => { }, + getWidgetAriaLabel: () => localize('modelPicker', "Model Picker") + }; + + // Get additional actions to show in the picker + const additionalActions = this.getAdditionalActions(); + let buttonBar: IAction[] = []; + + // If we have additional actions, add them to the button bar + if (additionalActions.length > 0) { + buttonBar = additionalActions; + } + + this.actionWidgetService.show( + 'modelPicker', + false, + items, + delegate, + anchor, + container, + buttonBar + ); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts similarity index 61% rename from src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts rename to src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts index 88721eb9795..d6a8b99f892 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts @@ -9,32 +9,30 @@ import { CHAT_CATEGORY } from '../../actions/chatActions.js'; import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { ChatContextKeys } from '../../../common/chatContextKeys.js'; import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; -import { PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; +import { runAttachInstructionsAction } from '../../actions/promptActions/index.js'; import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; -import { runAttachPromptAction } from '../../actions/reusablePromptActions/index.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { INSTRUCTIONS_LANGUAGE_ID } from '../../../common/promptSyntax/constants.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MenuId, MenuRegistry } from '../../../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../../editor/browser/services/codeEditorService.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; /** - * Command ID of the "Use Prompt" command. + * Command ID of the "Attach Instructions" command. */ -export const COMMAND_ID = 'workbench.command.prompts.use'; +export const INSTRUCTIONS_COMMAND_ID = 'workbench.command.instructions.attach'; /** - * Keybinding of the "Use Prompt" command. + * Keybinding of the "Use Instructions" command. * The `cmd + /` is the current keybinding for 'attachment', so we use - * the `alt` key modifier to convey the "prompt attachment" action. + * the `alt` key modifier to convey the "instructions attachment" action. */ -const COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; +const INSTRUCTIONS_COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; /** - * Implementation of the "Use Prompt" command. The command works in the following way. + * Implementation of the "Use Instructions" command. The command works in the following way. * * When executed, it tries to see if a `prompt file` was open in the active code editor * (see {@link IChatAttachPromptActionOptions.resource resource}), and if a chat input @@ -56,14 +54,14 @@ const command = async ( ): Promise => { const commandService = accessor.get(ICommandService); - await runAttachPromptAction({ - resource: getActivePromptUri(accessor), + await runAttachInstructionsAction(commandService, { + resource: getActiveInstructionsFileUri(accessor), widget: getFocusedChatWidget(accessor), - }, commandService); + }); }; /** - * Get chat widget reference to attach prompt to. + * Get chat widget reference to attach instructions to. */ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | undefined { const chatWidgetService = accessor.get(IChatWidgetService); @@ -82,69 +80,37 @@ export function getFocusedChatWidget(accessor: ServicesAccessor): IChatWidget | } /** - * Gets active editor instance, if any. + * Gets `URI` of a instructions file open in an active editor instance, if any. */ -export function getActiveCodeEditor(accessor: ServicesAccessor): IActiveCodeEditor | undefined { - const editorService = accessor.get(IEditorService); - const { activeTextEditorControl } = editorService; - - if (isCodeEditor(activeTextEditorControl) && activeTextEditorControl.hasModel()) { - return activeTextEditorControl; - } - - if (isDiffEditor(activeTextEditorControl)) { - const originalEditor = activeTextEditorControl.getOriginalEditor(); - if (!originalEditor.hasModel()) { - return undefined; - } - - return originalEditor; - } - - return undefined; -} - -/** - * Gets `URI` of a prompt file open in an active editor instance, if any. - */ -export const getActivePromptUri = ( +export const getActiveInstructionsFileUri = ( accessor: ServicesAccessor, ): URI | undefined => { - const activeEditor = getActiveCodeEditor(accessor); - if (!activeEditor) { - return undefined; - } - - const model = activeEditor.getModel(); - if (model.getLanguageId() === PROMPT_LANGUAGE_ID) { + const codeEditorService = accessor.get(ICodeEditorService); + const model = codeEditorService.getActiveCodeEditor()?.getModel(); + if (model?.getLanguageId() === INSTRUCTIONS_LANGUAGE_ID) { return model.uri; } - - if (isPromptFile(model.uri)) { - return model.uri; - } - return undefined; }; /** - * Register the "Use Prompt" command with its keybinding. + * Register the "Attach Instructions" command with its keybinding. */ KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: COMMAND_ID, + id: INSTRUCTIONS_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, - primary: COMMAND_KEY_BINDING, + primary: INSTRUCTIONS_COMMAND_KEY_BINDING, handler: command, when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), }); /** - * Register the "Use Prompt" command in the `command palette`. + * Register the "Use Instructions" command in the `command palette`. */ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { - id: COMMAND_ID, - title: localize('commands.prompts.use.title', "Use Prompt"), + id: INSTRUCTIONS_COMMAND_ID, + title: localize('attach-instructions.capitalized.ellipses', "Attach Instructions..."), category: CHAT_CATEGORY }, when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index 2cde08cb946..af2707a4e01 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -6,7 +6,7 @@ import { localize } from '../../../../../../../nls.js'; import { createPromptFile } from './utils/createPromptFile.js'; import { CHAT_CATEGORY } from '../../../actions/chatActions.js'; -import { askForPromptName } from './dialogs/askForPromptName.js'; +import { askForPromptFileName } from './dialogs/askForPromptName.js'; import { ChatContextKeys } from '../../../../common/chatContextKeys.js'; import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { askForPromptSourceFolder } from './dialogs/askForPromptSourceFolder.js'; @@ -17,7 +17,7 @@ import { PromptsConfig } from '../../../../../../../platform/prompts/common/conf import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; import { ContextKeyExpr } from '../../../../../../../platform/contextkey/common/contextkey.js'; import { MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js'; -import { IPromptPath, IPromptsService } from '../../../../common/promptSyntax/service/types.js'; +import { IPromptsService, TPromptsType } from '../../../../common/promptSyntax/service/types.js'; import { IQuickInputService } from '../../../../../../../platform/quickinput/common/quickInput.js'; import { ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; @@ -26,37 +26,12 @@ import { IUserDataSyncEnablementService, SyncResource } from '../../../../../../ import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../../../platform/notification/common/notification.js'; -/** - * Base command ID prefix. - */ -const BASE_COMMAND_ID = 'workbench.command.prompts.create'; - -/** - * Command ID for creating a 'local' prompt. - */ -const LOCAL_COMMAND_ID = `${BASE_COMMAND_ID}.local`; - -/** - * Command ID for creating a 'user' prompt. - */ -const USER_COMMAND_ID = `${BASE_COMMAND_ID}.user`; - -/** - * Title of the 'create local prompt' command. - */ -const LOCAL_COMMAND_TITLE = localize('commands.prompts.create.title.local', "Create Prompt"); - -/** - * Title of the 'create user prompt' command. - */ -const USER_COMMAND_TITLE = localize('commands.prompts.create.title.user', "Create User Prompt"); - /** * The command implementation. */ const command = async ( accessor: ServicesAccessor, - type: IPromptPath['type'], + type: TPromptsType, ): Promise => { const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); @@ -69,13 +44,19 @@ const command = async ( const workspaceService = accessor.get(IWorkspaceContextService); const userDataSyncEnablementService = accessor.get(IUserDataSyncEnablementService); - const fileName = await askForPromptName(type, quickInputService); - if (!fileName) { - return; - } + const placeHolder = (type === 'instructions') + ? localize( + 'workbench.command.instructions.create.location.placeholder', + "Select a location to create the instructions file in...", + ) + : localize( + 'workbench.command.prompt.create.location.placeholder', + "Select a location to create the prompt file in...", + ); const selectedFolder = await askForPromptSourceFolder({ - type: type, + type, + placeHolder, labelService, openerService, promptsService, @@ -87,13 +68,23 @@ const command = async ( return; } - const content = localize( - 'workbench.command.prompts.create.initial-content', - "Add prompt contents...", - ); + const fileName = await askForPromptFileName(type, quickInputService); + if (!fileName) { + return; + } + + const content = (type === 'instructions') + ? localize( + 'workbench.command.instructions.create.initial-content', + "Add instructions...", + ) + : localize( + 'workbench.command.prompt.create.initial-content', + "Add prompt contents...", + ); const promptUri = await createPromptFile({ fileName, - folder: selectedFolder, + folder: selectedFolder.uri, content, fileService, openerService, @@ -101,7 +92,7 @@ const command = async ( await openerService.open(promptUri); - if (type !== 'user') { + if (selectedFolder.storage !== 'user') { return; } @@ -148,55 +139,43 @@ const command = async ( ); }; -/** - * Factory for creating the command handler with specific prompt `type`. - */ -const commandFactory = (type: 'local' | 'user') => { - return async (accessor: ServicesAccessor): Promise => { - return command(accessor, type); - }; -}; +function register(type: TPromptsType, id: string, title: string) { + /** + * Register the command. + */ + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id, + weight: KeybindingWeight.WorkbenchContrib, + handler: async (accessor: ServicesAccessor): Promise => { + return command(accessor, type); + }, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), + }); -/** - * Register the "Create Prompt" command. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: LOCAL_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - handler: commandFactory('local'), - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), -}); + /** + * Register the command in the command palette. + */ + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id, + title, + category: CHAT_CATEGORY + }, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) + }); +} -/** - * Register the "Create User Prompt" command. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: USER_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - handler: commandFactory('user'), - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), -}); +export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt'; +export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions'; -/** - * Register the "Create Prompt" command in the command palette. - */ -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: LOCAL_COMMAND_ID, - title: LOCAL_COMMAND_TITLE, - category: CHAT_CATEGORY - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) -}); +register( + 'instructions', + NEW_INSTRUCTIONS_COMMAND_ID, + localize('commands.new.instructions.local.title', "New Instructions File...") +); +register( + 'prompt', + NEW_PROMPT_COMMAND_ID, + localize('commands.new.prompt.local.title', "New Prompt File...") +); -/** - * Register the "Create User Prompt" command in the command palette. - */ -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: USER_COMMAND_ID, - title: USER_COMMAND_TITLE, - category: CHAT_CATEGORY, - }, - when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) -}); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index cd412256291..494dcf0ce7e 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -4,25 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../../../nls.js'; -import { PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; +import { TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; +import { getPromptFileExtension } from '../../../../../../../../platform/prompts/common/constants.js'; import { IQuickInputService } from '../../../../../../../../platform/quickinput/common/quickInput.js'; /** - * Asks the user for a prompt name. + * Asks the user for a file name. */ -export const askForPromptName = async ( - _type: 'local' | 'user', +export const askForPromptFileName = async ( + type: TPromptsType, quickInputService: IQuickInputService, ): Promise => { - const result = await quickInputService.input( - { - placeHolder: localize( - 'commands.prompts.create.ask-name.placeholder', - "Provide a prompt name", - PROMPT_FILE_EXTENSION, - ), - }); + const placeHolder = (type === 'instructions') + ? localize('askForInstructionsFileName.placeholder', "Enter the name of the instructions file") + : localize('askForPromptFileName.placeholder', "Enter the name of the prompt file"); + const result = await quickInputService.input({ placeHolder }); if (!result) { return undefined; } @@ -32,9 +29,10 @@ export const askForPromptName = async ( return undefined; } - const cleanName = (trimmedName.endsWith(PROMPT_FILE_EXTENSION)) + const fileExtension = getPromptFileExtension(type); + const cleanName = (trimmedName.endsWith(fileExtension)) ? trimmedName - : `${trimmedName}${PROMPT_FILE_EXTENSION}`; + : `${trimmedName}${fileExtension}`; return cleanName; }; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts index 2f446a81472..bf71ed3e107 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts @@ -6,22 +6,21 @@ import { localize } from '../../../../../../../../nls.js'; import { URI } from '../../../../../../../../base/common/uri.js'; import { WithUriValue } from '../../../../../../../../base/common/types.js'; -import { DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; import { basename, extUri } from '../../../../../../../../base/common/resources.js'; -import { IPromptsService } from '../../../../../common/promptSyntax/service/types.js'; import { ILabelService } from '../../../../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; +import { PROMPT_DOCUMENTATION_URL } from '../../../../../common/promptSyntax/constants.js'; import { IWorkspaceContextService } from '../../../../../../../../platform/workspace/common/workspace.js'; +import { IPromptPath, IPromptsService, TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../../../../../platform/quickinput/common/quickInput.js'; /** * Options for {@link askForPromptSourceFolder} dialog. */ interface IAskForFolderOptions { - /** - * Prompt type. - */ - readonly type: 'local' | 'user'; + + readonly type: TPromptsType; + readonly placeHolder: string; readonly labelService: ILabelService; readonly openerService: IOpenerService; @@ -30,14 +29,18 @@ interface IAskForFolderOptions { readonly workspaceService: IWorkspaceContextService; } +interface IFolderQuickPickItem extends IQuickPickItem { + readonly folder: IPromptPath; +} + /** * Asks the user for a specific prompt folder, if multiple folders provided. * Returns immediately if only one folder available. */ export const askForPromptSourceFolder = async ( options: IAskForFolderOptions, -): Promise => { - const { type, promptsService, quickInputService, labelService, openerService, workspaceService } = options; +): Promise => { + const { type, placeHolder, promptsService, quickInputService, labelService, openerService, workspaceService } = options; // get prompts source folders based on the prompt type const folders = promptsService.getSourceFolders(type); @@ -52,20 +55,31 @@ export const askForPromptSourceFolder = async ( // if there is only one folder, no need to ask // note! when we add more actions to the dialog, this will have to go if (folders.length === 1) { - return folders[0].uri; + return folders[0]; } - const pickOptions: IPickOptions> = { - placeHolder: localize( - 'commands.prompts.create.ask-folder.placeholder', - "Select a prompt source folder", - ), + const pickOptions: IPickOptions = { + placeHolder, canPickMany: false, matchOnDescription: true, }; // create list of source folder locations - const foldersList = folders.map(({ uri }): WithUriValue => { + const foldersList = folders.map(folder => { + const uri = folder.uri; + if (folder.storage === 'user') { + return { + type: 'item', + label: localize( + 'commands.prompts.create.source-folder.user', + "User Data Folder", + ), + description: labelService.getUriLabel(uri), + tooltip: uri.fsPath, + folder + }; + } + const { folders } = workspaceService.getWorkspace(); const isMultirootWorkspace = (folders.length > 1); @@ -79,7 +93,7 @@ export const askForPromptSourceFolder = async ( label: basename(uri), description: labelService.getUriLabel(uri, { relative: true }), tooltip: uri.fsPath, - value: uri, + folder, }; } @@ -94,7 +108,7 @@ export const askForPromptSourceFolder = async ( // use absolute path as the description description: labelService.getUriLabel(uri, { relative: false }), tooltip: uri.fsPath, - value: uri, + folder, }; }); @@ -103,7 +117,7 @@ export const askForPromptSourceFolder = async ( return; } - return answer.value; + return answer.folder; }; /** @@ -122,9 +136,9 @@ const showNoFoldersDialog = async ( 'commands.prompts.create.ask-folder.empty.docs-label', 'Learn how to configure reusable prompts', ), - description: DOCUMENTATION_URL, - tooltip: DOCUMENTATION_URL, - value: URI.parse(DOCUMENTATION_URL), + description: PROMPT_DOCUMENTATION_URL, + tooltip: PROMPT_DOCUMENTATION_URL, + value: URI.parse(PROMPT_DOCUMENTATION_URL), }; const result = await quickInputService.pick( diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts index 4027531b5dc..dbea375a77d 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts @@ -10,7 +10,7 @@ import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; import { dirname } from '../../../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; import { IOpenerService } from '../../../../../../../../platform/opener/common/opener.js'; -import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; +import { isPromptOrInstructionsFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; /** * Options for the {@link createPromptFile} utility. @@ -52,7 +52,7 @@ export const createPromptFile = async ( const promptUri = URI.joinPath(folder, fileName); assert( - isPromptFile(promptUri), + isPromptOrInstructionsFile(promptUri), new InvalidPromptName(fileName), ); diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index d0a47a245b4..9fe6e516395 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -24,7 +24,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ChatContextKeys } from './chatContextKeys.js'; -import { IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; +import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -138,6 +138,7 @@ export interface IChatAgentRequest { rejectedConfirmationData?: any[]; userSelectedModelId?: string; userSelectedTools?: string[]; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IChatQuestion { diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index e761fc19358..efa8b7781de 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -24,6 +24,7 @@ export interface ICodeMapperCodeBlock { export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; readonly chatRequestId?: string; + readonly chatRequestModel?: string; readonly location?: string; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 1f58d5709ff..9129359e861 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -30,10 +30,10 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); - export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); + export const hasPromptFile = new RawContextKey('chatPromptFileAttached', false, { type: 'boolean', description: localize('chatPromptFileAttachedContextDescription', "True when the chat has a prompt file attached.") }); export const chatMode = new RawContextKey('chatMode', ChatMode.Ask, { type: 'string', description: localize('chatMode', "The current chat mode.") }); - export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection + export const supported = ContextKeyExpr.or(IsWebContext.negate(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); export const extensionParticipantRegistered = new RawContextKey('chatPanelExtensionParticipantRegistered', false, { type: 'boolean', description: localize('chatPanelExtensionParticipantRegistered', "True when a default chat participant is registered for the panel from an extension.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts index c2b5507f24c..94c1f8b4757 100644 --- a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -8,7 +8,7 @@ import { Barrier } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -21,7 +21,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asText, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; -import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { ChatContextKeys } from './chatContextKeys.js'; @@ -31,6 +31,7 @@ import Severity from '../../../../base/common/severity.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { isWeb } from '../../../../base/common/platform.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { Mutable } from '../../../../base/common/types.js'; export const IChatEntitlementService = createDecorator('chatEntitlementService'); @@ -58,18 +59,6 @@ export enum ChatSentiment { Installed = 3 } -export interface IChatQuotas { - readonly chatQuotaExceeded: boolean; - readonly completionsQuotaExceeded: boolean; - readonly quotaResetDate: Date | undefined; - - readonly chatTotal?: number; - readonly completionsTotal?: number; - - readonly chatRemaining?: number; - readonly completionsRemaining?: number; -} - export interface IChatEntitlementService { _serviceBrand: undefined; @@ -81,7 +70,7 @@ export interface IChatEntitlementService { readonly onDidChangeQuotaExceeded: Event; readonly onDidChangeQuotaRemaining: Event; - readonly quotas: IChatQuotas; + readonly quotas: IQuotas; update(token: CancellationToken): Promise; @@ -108,7 +97,7 @@ const defaultChat = { interface IChatQuotasAccessor { clearQuotas(): void; - acceptQuotas(quotas: IChatQuotas): void; + acceptQuotas(quotas: IQuotas): void; } export class ChatEntitlementService extends Disposable implements IChatEntitlementService { @@ -194,7 +183,7 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme private readonly _onDidChangeQuotaRemaining = this._register(new Emitter()); readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event; - private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }; + private _quotas: IQuotas = {}; get quotas() { return this._quotas; } private readonly chatQuotaExceededContextKey: IContextKey; @@ -206,69 +195,54 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme }; private registerListeners(): void { - const chatQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded]); - const completionsQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); + const quotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded, this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); + const cts = this._register(new MutableDisposable()); this._register(this.contextKeyService.onDidChangeContext(e => { - let changed = false; - if (e.affectsSome(chatQuotaExceededSet)) { - const newChatQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.chatQuotaExceeded); - if (typeof newChatQuotaExceeded === 'boolean' && newChatQuotaExceeded !== this._quotas.chatQuotaExceeded) { - this._quotas = { - ...this._quotas, - chatQuotaExceeded: newChatQuotaExceeded, - }; - changed = true; + if (e.affectsSome(quotaExceededSet)) { + if (cts.value) { + cts.value.cancel(); } - } - - if (e.affectsSome(completionsQuotaExceededSet)) { - const newCompletionsQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.completionsQuotaExceeded); - if (typeof newCompletionsQuotaExceeded === 'boolean' && newCompletionsQuotaExceeded !== this._quotas.completionsQuotaExceeded) { - this._quotas = { - ...this._quotas, - completionsQuotaExceeded: newCompletionsQuotaExceeded, - }; - changed = true; - } - } - - if (changed) { - this.updateContextKeys(); - this._onDidChangeQuotaExceeded.fire(); + cts.value = new CancellationTokenSource(); + this.update(cts.value.token); } })); } - acceptQuotas(quotas: IChatQuotas): void { + acceptQuotas(quotas: IQuotas): void { const oldQuota = this._quotas; this._quotas = quotas; this.updateContextKeys(); - if ( - oldQuota.chatQuotaExceeded !== this._quotas.chatQuotaExceeded || - oldQuota.completionsQuotaExceeded !== this._quotas.completionsQuotaExceeded - ) { + const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat); + const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions); + const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat); + + if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) { this._onDidChangeQuotaExceeded.fire(); } - if ( - oldQuota.chatRemaining !== this._quotas.chatRemaining || - oldQuota.completionsRemaining !== this._quotas.completionsRemaining - ) { + if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining) { this._onDidChangeQuotaRemaining.fire(); } } + private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } { + return { + changed: { + exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0), + remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining + } + }; + } + clearQuotas(): void { - if (this.quotas.chatQuotaExceeded || this.quotas.completionsQuotaExceeded) { - this.acceptQuotas({ chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }); - } + this.acceptQuotas({}); } private updateContextKeys(): void { - this.chatQuotaExceededContextKey.set(this._quotas.chatQuotaExceeded); - this.completionsQuotaExceededContextKey.set(this._quotas.completionsQuotaExceeded); + this.chatQuotaExceededContextKey.set(this._quotas.chat?.percentRemaining === 0); + this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0); } //#endregion @@ -301,8 +275,9 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme type EntitlementClassification = { tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; - quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; - quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat requests available to the user' }; + quotaPremiumChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of premium chat requests available to the user' }; + quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of code completions available to the user' }; quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; owner: 'bpasero'; comment: 'Reporting chat entitlements'; @@ -312,16 +287,21 @@ type EntitlementEvent = { entitlement: ChatEntitlement; tid: string; quotaChat: number | undefined; + quotaPremiumChat: number | undefined; quotaCompletions: number | undefined; quotaResetDate: string | undefined; }; -interface IEntitlementsResponse { - readonly access_type_sku: string; - readonly assigned_date: string; - readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; - readonly analytics_tracking_id: string; +interface IQuotaSnapshotResponse { + readonly entitlement: number; + readonly overage_count: number; + readonly overage_permitted: boolean; + readonly percent_remaining: number; + readonly remaining: number; + readonly unlimited: boolean; +} + +interface ILegacyQuotaSnapshotResponse { readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -330,7 +310,21 @@ interface IEntitlementsResponse { readonly chat: number; readonly completions: number; }; - readonly limited_user_reset_date: string; +} + +interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly chat_enabled: boolean; + readonly analytics_tracking_id: string; + readonly limited_user_reset_date?: string; // for Copilot Free + readonly quota_reset_date?: string; // for all other Copilot SKUs + readonly quota_snapshots?: { + chat?: IQuotaSnapshotResponse; + completions?: IQuotaSnapshotResponse; + premium_interactions?: IQuotaSnapshotResponse; + }; } interface IEntitlements { @@ -338,14 +332,21 @@ interface IEntitlements { readonly quotas?: IQuotas; } +export interface IQuotaSnapshot { + readonly total: number; + readonly percentRemaining: number; + + readonly overageEnabled: boolean; + readonly overageCount: number; + + readonly unlimited: boolean; +} + interface IQuotas { - readonly chatTotal?: number; - readonly completionsTotal?: number; - - readonly chatRemaining?: number; - readonly completionsRemaining?: number; - readonly resetDate?: string; + readonly chat?: IQuotaSnapshot; + readonly completions?: IQuotaSnapshot; + readonly premiumChat?: IQuotaSnapshot; } export class ChatEntitlementRequests extends Disposable { @@ -465,8 +466,17 @@ export class ChatEntitlementRequests extends Disposable { } private async doGetSessions(providerId: string): Promise { + const preferredAccountName = this.authenticationExtensionsService.getAccountPreference(defaultChat.chatExtensionId, providerId) ?? this.authenticationExtensionsService.getAccountPreference(defaultChat.extensionId, providerId); + let preferredAccount: AuthenticationSessionAccount | undefined; + for (const account of await this.authenticationService.getAccounts(providerId)) { + if (account.label === preferredAccountName) { + preferredAccount = account; + break; + } + } + try { - return await this.authenticationService.getSessions(providerId); + return await this.authenticationService.getSessions(providerId, undefined, preferredAccount); } catch (error) { // ignore - errors can throw if a provider is not registered } @@ -551,32 +561,82 @@ export class ChatEntitlementRequests extends Disposable { entitlement = ChatEntitlement.Unavailable; } - const chatRemaining = entitlementsResponse.limited_user_quotas?.chat; - const completionsRemaining = entitlementsResponse.limited_user_quotas?.completions; - const entitlements: IEntitlements = { entitlement, - quotas: { - chatTotal: entitlementsResponse.monthly_quotas?.chat, - completionsTotal: entitlementsResponse.monthly_quotas?.completions, - chatRemaining: typeof chatRemaining === 'number' ? Math.max(0, chatRemaining) : undefined, - completionsRemaining: typeof completionsRemaining === 'number' ? Math.max(0, completionsRemaining) : undefined, - resetDate: entitlementsResponse.limited_user_reset_date - } + quotas: this.toQuotas(entitlementsResponse) }; this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); this.telemetryService.publicLog2('chatInstallEntitlement', { entitlement: entitlements.entitlement, tid: entitlementsResponse.analytics_tracking_id, - quotaChat: entitlementsResponse.limited_user_quotas?.chat, - quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, - quotaResetDate: entitlementsResponse.limited_user_reset_date + quotaChat: entitlementsResponse?.quota_snapshots?.chat?.remaining, + quotaPremiumChat: entitlementsResponse?.quota_snapshots?.premium_interactions?.remaining, + quotaCompletions: entitlementsResponse?.quota_snapshots?.completions?.remaining, + quotaResetDate: entitlementsResponse.quota_reset_date ?? entitlementsResponse.limited_user_reset_date }); return entitlements; } + private toQuotas(response: IEntitlementsResponse): IQuotas { + const quotas: Mutable = { + resetDate: response.quota_reset_date ?? response.limited_user_reset_date + }; + + // Legacy Free SKU Quota + if (response.monthly_quotas?.chat && typeof response.limited_user_quotas?.chat === 'number') { + quotas.chat = { + total: response.monthly_quotas.chat, + percentRemaining: Math.round((response.limited_user_quotas.chat / response.monthly_quotas.chat) * 100), + overageEnabled: false, + overageCount: 0, + unlimited: false + }; + } + + if (response.monthly_quotas?.completions && typeof response.limited_user_quotas?.completions === 'number') { + quotas.completions = { + total: response.monthly_quotas.completions, + percentRemaining: Math.round((response.limited_user_quotas.completions / response.monthly_quotas.completions) * 100), + overageEnabled: false, + overageCount: 0, + unlimited: false + }; + } + + // New Quota Snapshot + if (response.quota_snapshots) { + for (const quotaType of ['chat', 'completions', 'premium_interactions'] as const) { + const rawQuotaSnapshot = response.quota_snapshots[quotaType]; + if (!rawQuotaSnapshot) { + continue; + } + const quotaSnapshot: IQuotaSnapshot = { + total: rawQuotaSnapshot.entitlement, + percentRemaining: rawQuotaSnapshot.percent_remaining, + overageEnabled: rawQuotaSnapshot.overage_permitted, + overageCount: rawQuotaSnapshot.overage_count, + unlimited: rawQuotaSnapshot.unlimited + }; + + switch (quotaType) { + case 'chat': + quotas.chat = quotaSnapshot; + break; + case 'completions': + quotas.completions = quotaSnapshot; + break; + case 'premium_interactions': + quotas.premiumChat = quotaSnapshot; + break; + } + } + } + + return quotas; + } + private async request(url: string, type: 'GET', body: undefined, session: AuthenticationSession, token: CancellationToken): Promise; private async request(url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; private async request(url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { @@ -605,15 +665,7 @@ export class ChatEntitlementRequests extends Disposable { this.context.update({ entitlement: this.state.entitlement }); if (state.quotas) { - this.chatQuotasAccessor.acceptQuotas({ - chatQuotaExceeded: typeof state.quotas.chatRemaining === 'number' ? state.quotas.chatRemaining <= 0 : false, - completionsQuotaExceeded: typeof state.quotas.completionsRemaining === 'number' ? state.quotas.completionsRemaining <= 0 : false, - quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : undefined, - chatTotal: state.quotas.chatTotal, - completionsTotal: state.quotas.completionsTotal, - chatRemaining: state.quotas.chatRemaining, - completionsRemaining: state.quotas.completionsRemaining - }); + this.chatQuotasAccessor.acceptQuotas(state.quotas); } } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d055ffe709a..7aedf0ee4c6 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; @@ -27,7 +28,7 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -76,7 +77,7 @@ export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVaria readonly isFile: true; readonly value: URI | Location | undefined; readonly isSelection: boolean; - readonly isPrompt: boolean; + readonly isPromptFile: boolean; enabled: boolean; } @@ -127,6 +128,15 @@ export interface IDiagnosticVariableEntryFilterData { readonly filterRange?: IRange; } +/** + * Chat variable that represents an attached prompt file. + */ +export interface IPromptVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'file'; + readonly value: URI | Location; + readonly isRoot: boolean; +} + export namespace IDiagnosticVariableEntryFilterData { export const icon = Codicon.error; @@ -145,8 +155,7 @@ export namespace IDiagnosticVariableEntryFilterData { name: label(data), icon, value: data, - kind: 'diagnostic' as const, - range: data.filterRange ? new OffsetRange(data.filterRange.startLineNumber, data.filterRange.endLineNumber) : undefined, + kind: 'diagnostic', ...data, }; } @@ -210,6 +219,10 @@ export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj return obj.kind === 'diagnostic'; } +export function isChatRequestFileEntry(obj: IChatRequestVariableEntry): obj is IChatRequestFileEntry { + return obj.kind === 'file'; +} + export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry { const entry = obj as IChatRequestVariableEntry; return typeof entry === 'object' && @@ -236,6 +249,7 @@ export interface IChatRequestModel { readonly attachedContext?: IChatRequestVariableEntry[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; + readonly editedFileEvents?: IChatAgentEditedFileEvent[]; shouldBeRemovedOnSend: IChatRequestDisablement | undefined; } @@ -286,7 +300,8 @@ export type IChatProgressHistoryResponseContent = | IChatTask | IChatTextEditGroup | IChatNotebookEditGroup - | IChatConfirmation; + | IChatConfirmation + | IChatExtensionsContent; /** * "Normal" progress kinds that are rendered as parts of the stream of content. @@ -376,6 +391,7 @@ export interface IChatRequestModelParameters { isCompleteAddedRequest?: boolean; modelId?: string; restoredId?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export class ChatRequestModel implements IChatRequestModel { @@ -393,6 +409,7 @@ export class ChatRequestModel implements IChatRequestModel { private readonly _confirmation?: string; private readonly _locationData?: IChatLocationData; private readonly _attachedContext?: IChatRequestVariableEntry[]; + private readonly _editedFileEvents?: IChatAgentEditedFileEvent[]; public get session(): ChatModel { return this._session; @@ -430,6 +447,10 @@ export class ChatRequestModel implements IChatRequestModel { return this._attachedContext; } + public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined { + return this._editedFileEvents; + } + constructor(params: IChatRequestModelParameters) { this._session = params.session; this.message = params.message; @@ -442,6 +463,7 @@ export class ChatRequestModel implements IChatRequestModel { this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; this.modelId = params.modelId; this.id = params.restoredId ?? 'request_' + generateUuid(); + this._editedFileEvents = params.editedFileEvents; } adoptTo(session: ChatModel) { @@ -510,6 +532,7 @@ class AbstractResponse implements IResponse { case 'codeblockUri': case 'toolInvocation': case 'toolInvocationSerialized': + case 'extensions': case 'undoStop': // Ignore continue; @@ -1068,6 +1091,7 @@ export interface ISerializableChatRequestData { codeCitations?: ReadonlyArray; timestamp?: number; confirmation?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IExportableChatData { @@ -1423,7 +1447,37 @@ export class ChatModel extends Disposable implements IChatModel { this.chatEditingService.startOrContinueGlobalEditingSession(this) : this.chatEditingService.createEditingSession(this); this._editingSession = new ObservablePromise(editingSessionPromise); - this._editingSession.promise.then(editingSession => this._store.isDisposed ? editingSession.dispose() : this._register(editingSession)); + this._editingSession.promise.then(editingSession => { + this._store.isDisposed ? editingSession.dispose() : this._register(editingSession); + + // const currentStates = new ResourceMap(); + // this._register(autorun(r => { + // editingSession.entries.read(r).forEach(entry => { + // const state = entry.state.read(r); + // if (state !== currentStates.get(entry.modifiedURI)) { + // currentStates.set(entry.modifiedURI, state); + // if (state === ModifiedFileEntryState.Rejected) { + // this.currentWorkingSetEntries.push({ + // uri: entry.modifiedURI, + // state: ChatAgentWorkingSetEntryState.Rejected + // }); + // } + // } + // }); + // })); + }); + } + + private currentEditedFileEvents = new ResourceMap(); + notifyEditingAction(action: IChatEditingSessionAction): void { + const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep : + action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo : + action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null; + if (state === null) { + return; + } + + this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri }); } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -1449,6 +1503,7 @@ export class ChatModel extends Disposable implements IChatModel { timestamp: raw.timestamp ?? -1, restoredId: raw.requestId, confirmation: raw.confirmation, + editedFileEvents: raw.editedFileEvents, }); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.response || raw.result || (raw as any).responseErrorDetails) { @@ -1596,6 +1651,8 @@ export class ChatModel extends Disposable implements IChatModel { } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string): ChatRequestModel { + const editedFileEvents = [...this.currentEditedFileEvents.values()]; + this.currentEditedFileEvents.clear(); const request = new ChatRequestModel({ session: this, message, @@ -1606,7 +1663,8 @@ export class ChatModel extends Disposable implements IChatModel { locationData, attachedContext: attachments, isCompleteAddedRequest, - modelId + modelId, + editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined, }); request.response = new ChatResponseModel({ responseContent: [], @@ -1676,6 +1734,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'warning' || progress.kind === 'progressTask' || progress.kind === 'confirmation' || + progress.kind === 'extensions' || progress.kind === 'toolInvocation' ) { request.response.updateContent(progress, quiet); @@ -1789,6 +1848,7 @@ export class ChatModel extends Disposable implements IChatModel { codeCitations: r.response?.codeCitations, timestamp: r.timestamp, confirmation: r.confirmation, + editedFileEvents: r.editedFileEvents, }; }), }; @@ -1867,3 +1927,14 @@ export function getCodeCitationsMessage(citations: ReadonlyArray((this.variableService.getSelectedTools(sessionId)).filter(t => t.toolReferenceName).map(t => [t.toolReferenceName!, t])); let lineNumber = 1; let column = 1; @@ -43,7 +45,7 @@ export class ChatRequestParser { let newPart: IParsedChatRequestPart | undefined; if (previousChar.match(/\s/) || i === 0) { if (char === chatVariableLeader) { - newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); + newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName); } else if (char === chatAgentLeader) { newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { @@ -141,7 +143,7 @@ export class ChatRequestParser { return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); } - private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestToolPart | undefined { + private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray, toolsByName: ReadonlyMap): ChatRequestAgentPart | ChatRequestToolPart | undefined { const nextVariableMatch = message.match(variableReg); if (!nextVariableMatch) { return; @@ -151,7 +153,7 @@ export class ChatRequestParser { const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const tool = this.toolsService.getToolByName(name); + const tool = toolsByName.get(name); if (tool && tool.canBeReferencedInPrompt) { return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon); } @@ -159,7 +161,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | ChatRequestSlashPromptPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; @@ -208,8 +210,13 @@ export class ChatRequestParser { return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); } } - } + // if there's no agent, check if it's a prompt command + const promptCommand = this.promptsService.asPromptSlashCommand(command); + if (promptCommand) { + return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, promptCommand); + } + } return; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5a8f1e0bdd0..ec1d6c7b97e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -255,9 +255,15 @@ export interface IChatToolInvocationSerialized { isConfirmed: boolean | undefined; isComplete: boolean; toolCallId: string; + toolId: string; kind: 'toolInvocationSerialized'; } +export interface IChatExtensionsContent { + extensions: string[]; + kind: 'extensions'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -278,6 +284,7 @@ export type IChatProgress = | IChatConfirmation | IChatToolInvocation | IChatToolInvocationSerialized + | IChatExtensionsContent | IChatUndoStop; export interface IChatFollowup { @@ -374,7 +381,7 @@ export interface IChatEditingSessionAction { kind: 'chatEditingSessionAction'; uri: URI; hasRemainingEdits: boolean; - outcome: 'accepted' | 'rejected' | 'saved'; + outcome: 'accepted' | 'rejected' | 'userModified'; } export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction; @@ -474,11 +481,6 @@ export interface IChatSendRequestOptions { * The label of the confirmation action that was selected. */ confirmation?: string; - - /** - * Flag to indicate whether a prompt instructions attachment is present. - */ - hasInstructionAttachments?: boolean; } export const IChatService = createDecorator('IChatService'); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 44c67d2cff5..0b8627d26cf 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -27,7 +27,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; -import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, isImageVariableEntry, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; +import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, isImageVariableEntry, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from './chatParserTypes.js'; import { ChatRequestParser } from './chatRequestParser.js'; import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; @@ -35,7 +35,6 @@ import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatTransferService } from './chatTransferService.js'; -import { IChatVariablesService } from './chatVariables.js'; import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; import { ILanguageModelToolsService } from './languageModelToolsService.js'; @@ -154,7 +153,6 @@ export class ChatService extends Disposable implements IChatService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @@ -290,6 +288,12 @@ export class ChatService extends Disposable implements IChatService { notifyUserAction(action: IChatUserActionEvent): void { this._chatServiceTelemetry.notifyUserAction(action); this._onDidPerformUserAction.fire(action); + if (action.action.kind === 'chatEditingSessionAction') { + const model = this._sessionModels.get(action.sessionId); + if (model) { + model.notifyEditingAction(action.action); + } + } } async setChatSessionTitle(sessionId: string, title: string): Promise { @@ -580,7 +584,6 @@ export class ChatService extends Disposable implements IChatService { ...options, locationData: request.locationData, attachedContext: request.attachedContext, - hasInstructionAttachments: options?.hasInstructionAttachments ?? false, }; await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; } @@ -588,13 +591,8 @@ export class ChatService extends Disposable implements IChatService { async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - // if text is not provided, but chat input has `prompt instructions` - // attached, use the default prompt text to avoid empty messages - if (!request.trim() && options?.hasInstructionAttachments) { - request = 'Follow these instructions.'; - } - if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.hasInstructionAttachments) { + if (!request.trim() && !options?.slashCommand && !options?.agentId) { this.trace('sendRequest', 'Rejected empty message'); return; } @@ -746,7 +744,7 @@ export class ChatService extends Disposable implements IChatService { variableData = chatRequest.variableData; message = getPromptText(request.message).message; } else { - variableData = this.chatVariablesService.resolveVariables(parsedRequest, request.attachedContext); + variableData = { variables: this.prepareContext(request.attachedContext) }; model.updateRequest(request, variableData); const promptTextResult = getPromptText(request.message); @@ -769,7 +767,8 @@ export class ChatService extends Disposable implements IChatService { acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, - userSelectedTools: options?.userSelectedTools + userSelectedTools: options?.userSelectedTools, + editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; @@ -807,17 +806,19 @@ export class ChatService extends Disposable implements IChatService { agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined; } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { - request = model.addRequest(parsedRequest, { variables: [] }, attempt); - completeResponseCreated(); + if (commandPart.slashCommand.silent !== true) { + request = model.addRequest(parsedRequest, { variables: [] }, attempt); + completeResponseCreated(); + } // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; - for (const request of model.getRequests()) { - if (!request.response) { + for (const modelRequest of model.getRequests()) { + if (!modelRequest.response) { continue; } - history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: request.message.text }] }); - history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: request.response.response.toString() }] }); + history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] }); + history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] }); } const message = parsedRequest.text; const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { @@ -918,6 +919,27 @@ export class ChatService extends Disposable implements IChatService { }; } + private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] { + attachedContextVariables ??= []; + + // "reverse", high index first so that replacement is simple + attachedContextVariables.sort((a, b) => { + // If either range is undefined, sort it to the back + if (!a.range && !b.range) { + return 0; // Keep relative order if both ranges are undefined + } + if (!a.range) { + return 1; // a goes after b + } + if (!b.range) { + return -1; // a goes before b + } + return b.range.start - a.range.start; + }); + + return attachedContextVariables; + } + private async checkAgentAllowed(agent: IChatAgentData): Promise { if (agent.modes.includes(ChatMode.Agent)) { const enabled = await this.experimentService.getTreatment('chatAgentEnabled'); @@ -983,7 +1005,8 @@ export class ChatService extends Disposable implements IChatService { message: promptTextResult.message, command: request.response.slashCommand?.name, variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack - location: ChatAgentLocation.Panel + location: ChatAgentLocation.Panel, + editedFileEvents: request.editedFileEvents, }; history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} }); } diff --git a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts index 1da03b0d8e0..68480c1dddb 100644 --- a/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/common/chatSlashCommands.ts @@ -24,6 +24,16 @@ export interface IChatSlashData { * as it is entered. Defaults to `false`. */ executeImmediately?: boolean; + + /** + * Whether the command should be added as a request/response + * turn to the chat history. Defaults to `false`. + * + * For instance, the `/save` command opens an untitled document + * to the side hence does not contain any chatbot responses. + */ + silent?: boolean; + locations: ChatAgentLocation[]; modes?: ChatMode[]; } diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 2f0b193be18..a5f936c7360 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -9,9 +9,9 @@ import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { Location } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js'; -import { IParsedChatRequest } from './chatParserTypes.js'; +import { IChatModel, IDiagnosticVariableEntryFilterData } from './chatModel.js'; import { IChatContentReference, IChatProgressMessage } from './chatService.js'; +import { IToolData } from './languageModelToolsService.js'; export interface IChatVariableData { id: string; @@ -46,11 +46,7 @@ export const IChatVariablesService = createDecorator('ICh export interface IChatVariablesService { _serviceBrand: undefined; getDynamicVariables(sessionId: string): ReadonlyArray; - - /** - * Resolves all variables that occur in `prompt` - */ - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData; + getSelectedTools(sessionId: string): ReadonlyArray; } export interface IDynamicVariable { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index a152eb57eb8..cab2fd082c2 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -137,6 +137,7 @@ export interface IChatReferences { export interface IChatWorkingProgress { kind: 'working'; isPaused: boolean; + setPaused(paused: boolean): void; } /** diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index b5667355a79..5b9a13213d9 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -44,6 +44,7 @@ export interface IToolData { export type ToolDataSource = | { type: 'extension'; + label: string; extensionId: ExtensionIdentifier; /** * True for tools contributed through extension API from third-party extensions, so they can be disabled by policy. @@ -51,7 +52,12 @@ export type ToolDataSource = */ isExternalTool: boolean; } - | { type: 'mcp'; collectionId: string; definitionId: string } + | { + type: 'mcp'; + label: string; + collectionId: string; + definitionId: string; + } | { type: 'internal' }; export namespace ToolDataSource { @@ -132,6 +138,7 @@ export interface IPreparedToolInvocation { pastTenseMessage?: string | IMarkdownString; confirmationMessages?: IToolConfirmationMessages; presentation?: 'hidden' | undefined; + // When this gets extended, be sure to update `chatResponseAccessibleView.ts` to handle the new properties. toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData; } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index e89f5feb1c6..bbec0440163 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -36,6 +36,12 @@ export interface IChatMessageImagePart { value: IChatImageURLPart; } +export interface IChatMessageExtraDataPart { + type: 'extra_data'; + kind: string; + data: any; +} + export interface IChatImageURLPart { /** * The image's MIME type (e.g., "image/png", "image/jpeg"). @@ -75,7 +81,7 @@ export interface IChatMessageToolResultPart { isError?: boolean; } -export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart; +export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageExtraDataPart; export interface IChatMessage { readonly name?: string | undefined; @@ -119,6 +125,7 @@ export interface ILanguageModelChatMetadata { readonly id: string; readonly vendor: string; readonly version: string; + readonly description?: string; readonly family: string; readonly maxInputTokens: number; readonly maxOutputTokens: number; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts index 7522f5acd0b..7a48fc44494 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts @@ -11,15 +11,15 @@ import { ICodec } from '../../../../../../base/common/codecs/types/ICodec.js'; /** * `ChatPromptCodec` type is a `ICodec` with specific types for * stream messages and return types of the `encode`/`decode` functions. - * @see {@linkcode ICodec} + * @see {@link ICodec} */ interface IChatPromptCodec extends ICodec { /** * Decode a stream of `VSBuffer`s into a stream of `TChatPromptToken`s. * - * @see {@linkcode TChatPromptToken} - * @see {@linkcode VSBuffer} - * @see {@linkcode ChatPromptDecoder} + * @see {@link TChatPromptToken} + * @see {@link VSBuffer} + * @see {@link ChatPromptDecoder} */ decode: (value: ReadableStream) => ChatPromptDecoder; } @@ -31,8 +31,8 @@ export const ChatPromptCodec: IChatPromptCodec = Object.freeze({ /** * Encode a stream of `TChatPromptToken`s into a stream of `VSBuffer`s. * - * @see {@linkcode ReadableStream} - * @see {@linkcode VSBuffer} + * @see {@link ReadableStream} + * @see {@link VSBuffer} */ encode: (_stream: ReadableStream): ReadableStream => { throw new Error('The `encode` method is not implemented.'); @@ -41,10 +41,10 @@ export const ChatPromptCodec: IChatPromptCodec = Object.freeze({ /** * Decode a of `VSBuffer`s into a readable of `TChatPromptToken`s. * - * @see {@linkcode TChatPromptToken} - * @see {@linkcode VSBuffer} - * @see {@linkcode ChatPromptDecoder} - * @see {@linkcode ReadableStream} + * @see {@link TChatPromptToken} + * @see {@link VSBuffer} + * @see {@link ChatPromptDecoder} + * @see {@link ReadableStream} */ decode: (stream: ReadableStream): ChatPromptDecoder => { return new ChatPromptDecoder(stream); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts index 0f25d3da6e6..4dd0c536640 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/constants.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageFilter } from '../../../../../editor/common/languageSelector.js'; +import { LanguageSelector } from '../../../../../editor/common/languageSelector.js'; /** * Documentation link for the reusable prompts feature. */ -export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; +export const PROMPT_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; +export const INSTRUCTIONS_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-custom-instructions'; /** * Language ID for the reusable prompt syntax. @@ -16,8 +17,11 @@ export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; export const PROMPT_LANGUAGE_ID = 'prompt'; /** - * Prompt files language selector. + * Language ID for instructions syntax. */ -export const LANGUAGE_SELECTOR: LanguageFilter = Object.freeze({ - language: PROMPT_LANGUAGE_ID, -}); +export const INSTRUCTIONS_LANGUAGE_ID = 'instructions'; + +/** + * Prompt and instructions files language selector. + */ +export const PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR: LanguageSelector = [PROMPT_LANGUAGE_ID, INSTRUCTIONS_LANGUAGE_ID]; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index e8294ea8e9a..5eb58c77af6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -10,7 +10,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; +import { isPromptOrInstructionsFile } from '../../../../../../platform/prompts/common/constants.js'; import { OpenFailed, NotPromptFile, ResolveError, FolderReference } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; @@ -114,7 +114,7 @@ export class FilePromptContentProvider extends PromptContentsProviderBase[] = Obje /** * Prompt syntax decorations provider for text models. */ -export class TextModelPromptDecorator extends ProviderInstanceBase { +export class PromptDecorator extends ProviderInstanceBase { /** * Currently active decorations. */ @@ -49,7 +49,7 @@ export class TextModelPromptDecorator extends ProviderInstanceBase { await this.parser.allSettled(); this.removeAllDecorations(); - this.addDecorations(this.parser.tokens); + this.addDecorations(); return this; } @@ -117,14 +117,14 @@ export class TextModelPromptDecorator extends ProviderInstanceBase { /** * Add a decorations for all prompt tokens. */ - private addDecorations( - tokens: readonly BaseToken[], - ): this { - if (tokens.length === 0) { - return this; - } - + private addDecorations(): this { this.model.changeDecorations((accessor) => { + const { tokens } = this.parser; + + if (tokens.length === 0) { + return; + } + for (const token of tokens) { for (const Decoration of SUPPORTED_DECORATIONS) { if (Decoration.handles(token) === false) { @@ -189,8 +189,8 @@ registerThemingParticipant((_theme, collector) => { /** * Provider for prompt syntax decorators on text models. */ -export class PromptDecorationsProviderInstanceManager extends ProviderInstanceManagerBase { +export class PromptDecorationsProviderInstanceManager extends ProviderInstanceManagerBase { protected override get InstanceClass() { - return TextModelPromptDecorator; + return PromptDecorator; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts similarity index 78% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.d.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts index 51aa920498b..a7ef26521d2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRange } from '../../../../../../../../editor/common/core/range.ts'; -import { ModelDecorationOptions } from '../../../../../../../../editor/common/model/textModel.ts'; +import { IRange } from '../../../../../../../../editor/common/core/range.js'; +import { ModelDecorationOptions } from '../../../../../../../../editor/common/model/textModel.js'; /** * Decoration object. @@ -35,3 +35,14 @@ export enum DecorationClassNames { */ fileReference = DecorationClassNames.default, } + +/** + * Decoration CSS class modifiers. + */ +export enum CssClassModifiers { + /** + * CSS class modifier for `active` state of + * a `reactive` prompt syntax decoration. + */ + inactive = '.prompt-decoration-inactive', +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts index 24583cdd9b4..5630fb95e60 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts @@ -9,6 +9,7 @@ import { PromptPathAutocompletion } from './promptPathAutocompletion.js'; import { Registry } from '../../../../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../../../../services/lifecycle/common/lifecycle.js'; import { PromptLinkDiagnosticsInstanceManager } from './promptLinkDiagnosticsProvider.js'; +import { PromptHeaderDiagnosticsInstanceManager } from './promptHeaderDiagnosticsProvider.js'; import { BrandedService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { PromptDecorationsProviderInstanceManager } from './decorationsProvider/promptDecorationsProvider.js'; import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../../../common/contributions.js'; @@ -16,7 +17,7 @@ import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } f /** * Whether to enable decorations in the prompt editor. */ -export const DECORATIONS_ENABLED = false; +export const DECORATIONS_ENABLED = true; /** * Register all language features related to reusable prompts files. @@ -24,6 +25,7 @@ export const DECORATIONS_ENABLED = false; export const registerReusablePromptLanguageFeatures = () => { registerContribution(PromptLinkProvider); registerContribution(PromptLinkDiagnosticsInstanceManager); + registerContribution(PromptHeaderDiagnosticsInstanceManager); if (DECORATIONS_ENABLED) { registerContribution(PromptDecorationsProviderInstanceManager); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts new file mode 100644 index 00000000000..cb5410fd7c7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../../service/types.js'; +import { ProviderInstanceBase } from './providerInstanceBase.js'; +import { assertNever } from '../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js'; +import { TDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../../parsers/promptHeader/diagnostics.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; + +/** + * Unique ID of the markers provider class. + */ +const MARKERS_OWNER_ID = 'prompts-header-diagnostics-provider'; + +/** + * Prompt header diagnostics provider for an individual text model + * of a prompt file. + */ +class PromptHeaderDiagnosticsProvider extends ProviderInstanceBase { + constructor( + model: ITextModel, + @IPromptsService promptsService: IPromptsService, + @IMarkerService private readonly markerService: IMarkerService, + ) { + super(model, promptsService); + } + + /** + * Update diagnostic markers for the current editor. + */ + protected override async onPromptParserUpdate(): Promise { + // ensure that parsing process is settled + await this.parser.allSettled(); + + // clean up all previously added markers + this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); + + const { header } = this.parser; + + if (header === undefined) { + return this; + } + + const markers: IMarkerData[] = []; + for (const link of header.diagnostics) { + markers.push(toMarker(link)); + } + + this.markerService.changeOne( + MARKERS_OWNER_ID, + this.model.uri, + markers, + ); + + return this; + } + + /** + * Returns a string representation of this object. + */ + public override toString() { + return `prompt-link-diagnostics:${this.model.uri.path}`; + } +} + +/** + * Convert a provided diagnostic object into a marker data object. + */ +const toMarker = ( + diagnostic: TDiagnostic, +): IMarkerData => { + if (diagnostic instanceof PromptMetadataWarning) { + return { + message: diagnostic.message, + severity: MarkerSeverity.Warning, + ...diagnostic.range, + }; + } + + if (diagnostic instanceof PromptMetadataError) { + return { + message: diagnostic.message, + severity: MarkerSeverity.Error, + ...diagnostic.range, + }; + } + + + assertNever( + diagnostic, + `Unknown prompt metadata diagnostic type '${diagnostic}'.`, + ); +}; + +/** + * The class that manages creation and disposal of {@link PromptHeaderDiagnosticsProvider} + * classes for each specific editor text model. + */ +export class PromptHeaderDiagnosticsInstanceManager extends ProviderInstanceManagerBase { + protected override get InstanceClass() { + return PromptHeaderDiagnosticsProvider; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts index bfca30a73ea..29400894fa1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts @@ -16,7 +16,7 @@ import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../. /** * Unique ID of the markers provider class. */ -const MARKERS_OWNER_ID = 'reusable-prompts-syntax'; +const MARKERS_OWNER_ID = 'prompt-link-diagnostics-provider'; /** * Prompt links diagnostics provider for a single text model. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts index e4e9c612292..362126eb3d9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LANGUAGE_SELECTOR } from '../../constants.js'; import { IPromptsService } from '../../service/types.js'; import { assert } from '../../../../../../../base/common/assert.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { CancellationError } from '../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; import { FolderReference, NotPromptFile } from '../../../promptFileReferenceErrors.js'; import { ILink, ILinksList, LinkProvider } from '../../../../../../../editor/common/languages.js'; @@ -25,7 +25,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider { ) { super(); - this._register(this.languageService.linkProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.linkProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts index b3a3e919261..1a3dc385651 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts @@ -14,7 +14,6 @@ * - add `Windows` support */ -import { LANGUAGE_SELECTOR } from '../../constants.js'; import { IPromptsService } from '../../service/types.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { extUri } from '../../../../../../../base/common/resources.js'; @@ -22,6 +21,7 @@ import { assertOneOf } from '../../../../../../../base/common/types.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { CancellationError } from '../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; import { Position } from '../../../../../../../editor/common/core/position.js'; import { IPromptFileReference, IPromptReference } from '../../parsers/types.js'; import { assert, assertNever } from '../../../../../../../base/common/assert.js'; @@ -102,7 +102,7 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt ) { super(); - this._register(this.languageService.completionProvider.register(LANGUAGE_SELECTOR, this)); + this._register(this.languageService.completionProvider.register(PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR, this)); } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts index 222fe6f97c8..22fa6b89210 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PROMPT_LANGUAGE_ID } from '../../constants.js'; import { ProviderInstanceBase } from './providerInstanceBase.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { ObjectCache } from '../../../../../../../base/common/objectCache.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../constants.js'; import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; @@ -96,15 +96,15 @@ export abstract class ProviderInstanceManagerBase { const { model, oldLanguageId } = event; - // if language is set to `prompt` language, handle that model + // if language is set to `prompt` or `instructions` language, handle that model if (isPromptFileModel(model)) { this.instances.get(model); return; } - // if the language is changed away from `prompt`, + // if the language is changed away from `prompt` or `instructions`, // remove and dispose provider for this model - if (oldLanguageId === PROMPT_LANGUAGE_ID) { + if (isPromptOrInstructionsFile(oldLanguageId)) { this.instances.remove(model, true); return; } @@ -133,6 +133,16 @@ export abstract class ProviderInstanceManagerBase { + return (languageId === PROMPT_LANGUAGE_ID) || (languageId === INSTRUCTIONS_LANGUAGE_ID); +}; + /** * Check if a provided model is used for prompt files. */ @@ -148,7 +158,7 @@ const isPromptFileModel = ( return false; } - if (model.getLanguageId() !== PROMPT_LANGUAGE_ID) { + if (isPromptOrInstructionsFile(model.getLanguageId()) === false) { return false; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 11ded1875d7..dac5234b67f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TopError } from './topError.js'; +import { PromptHeader } from './promptHeader/header.js'; import { URI } from '../../../../../../base/common/uri.js'; import { PromptToken } from '../codecs/tokens/promptToken.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; @@ -12,21 +13,22 @@ import { FileReference } from '../codecs/tokens/fileReference.js'; import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; import { assertDefined } from '../../../../../../base/common/types.js'; import { IPromptContentsProvider } from '../contentProviders/types.js'; -import { IPromptReference, IResolveError, ITopError } from './types.js'; import { DeferredPromise } from '../../../../../../base/common/async.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { PromptVariableWithData } from '../codecs/tokens/promptVariable.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; import { assert, assertNever } from '../../../../../../base/common/assert.js'; import { BaseToken } from '../../../../../../editor/common/codecs/baseToken.js'; -import { IRange, Range } from '../../../../../../editor/common/core/range.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { basename, dirname, extUri } from '../../../../../../base/common/resources.js'; +import { IPromptMetadata, IPromptReference, IResolveError, ITopError } from './types.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { isPromptOrInstructionsFile } from '../../../../../../platform/prompts/common/constants.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; import { MarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownToken.js'; +import { FrontMatterHeader } from '../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; /** @@ -55,6 +57,20 @@ export class BasePromptParser */ private readonly _references: IPromptReference[] = []; + /** + * Reference to the prompt header object that holds metadata associated + * with the prompt. + */ + private promptHeader?: PromptHeader; + + /** + * Reference to the prompt header object that holds metadata associated + * with the prompt. + */ + public get header(): PromptHeader | undefined { + return this.promptHeader; + } + /** * The event is fired when lines or their content change. */ @@ -123,6 +139,12 @@ export class BasePromptParser return this; } + // by the time when the `firstParseResult` promise is resolved, + // this object may have been already disposed, hence noop + if (this.disposed) { + return this; + } + assertDefined( this.stream, 'No stream reference found.', @@ -130,6 +152,11 @@ export class BasePromptParser await this.stream.settled; + // if prompt header exists, also wait for it to be settled + if (this.promptHeader) { + await this.promptHeader.settled; + } + return this; } @@ -221,6 +248,10 @@ export class BasePromptParser delete this._errorCondition; this.receivedTokens = []; + // cleanup current prompt header object + this.promptHeader?.dispose(); + delete this.promptHeader; + // dispose all currently existing references this.disposeReferences(); @@ -246,6 +277,13 @@ export class BasePromptParser this.receivedTokens.push(token); } + // if a prompt header token received, create a new prompt header instance + if (token instanceof FrontMatterHeader) { + this.promptHeader = new PromptHeader(token.contentToken); + this.promptHeader.start(); + return; + } + // try to convert a prompt variable with data token into a file reference if (token instanceof PromptVariableWithData) { try { @@ -453,6 +491,67 @@ export class BasePromptParser .map(child => child.uri); } + /** + * Metadata defined in the prompt header. + */ + public get metadata(): IPromptMetadata { + if (this.header === undefined) { + return {}; + } + + const { metadata } = this.header; + if (metadata === undefined) { + return {}; + } + + const result: IPromptMetadata = {}; + + const { tools, description } = metadata; + if (tools !== undefined) { + result.tools = tools.toolNames; + } + + if (description !== undefined) { + result.description = description.text ?? undefined; + } + + return result; + } + + /** + * Entire associated `tools` metadata for this reference and + * all possible nested child references. + */ + public get allToolsMetadata(): readonly string[] | null { + let hasTools = false; + const result: string[] = []; + + const { tools } = this.metadata; + + if (tools !== undefined) { + result.push(...tools); + hasTools = true; + } + + for (const reference of this.references) { + const { allToolsMetadata } = reference; + + if (allToolsMetadata === null) { + continue; + } + + result.push(...allToolsMetadata); + hasTools = true; + } + + if (hasTools === false) { + return null; + } + + // return unique list of tools + return [...new Set(result)]; + } + /** * Get list of errors for the direct links of the current reference. */ @@ -553,7 +652,7 @@ export class BasePromptParser * Check if the current reference points to a prompt snippet file. */ public get isPromptFile(): boolean { - return isPromptFile(this.uri); + return isPromptOrInstructionsFile(this.uri); } /** @@ -572,7 +671,13 @@ export class BasePromptParser } this.disposeReferences(); + this.stream?.dispose(); + delete this.stream; + + this.promptHeader?.dispose(); + delete this.promptHeader; + this._onUpdate.fire(); super.dispose(); @@ -727,6 +832,14 @@ export class PromptReference extends ObservableDisposable implements IPromptRefe return this.parser.allReferences; } + public get metadata(): IPromptMetadata { + return this.parser.metadata; + } + + public get allToolsMetadata(): readonly string[] | null { + return this.parser.allToolsMetadata; + } + public get allValidReferences(): readonly IPromptReference[] { return this.parser.allValidReferences; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts new file mode 100644 index 00000000000..754293f6dcf --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../../../../../editor/common/core/range.js'; + +/** + * List of all currently supported diagnostic types. + */ +export type TDiagnostic = PromptMetadataWarning | PromptMetadataError; + +/** + * Diagnostics object that hold information about some issue + * related to the prompt header metadata. + */ +export abstract class PromptMetadataDiagnostic { + constructor( + public readonly range: Range, + public readonly message: string, + ) { } + + /** + * String representation of the diagnostic object. + */ + public abstract toString(): string; +} + +/** + * Diagnostics object that hold information about some + * non-fatal issue related to the prompt header metadata. + */ +export class PromptMetadataWarning extends PromptMetadataDiagnostic { + public override toString(): string { + return `warning(${this.message})${this.range}`; + } +} + +/** + * Diagnostics object that hold information about some + * fatal issue related to the prompt header metadata. + */ +export class PromptMetadataError extends PromptMetadataDiagnostic { + public override toString(): string { + return `error(${this.message})${this.range}`; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts new file mode 100644 index 00000000000..5e6f4829681 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/header.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../../nls.js'; +import { PromptToolsMetadata } from './metadata/tools.js'; +import { PromptDescriptionMetadata } from './metadata/description.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { Text } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { PromptMetadataError, PromptMetadataWarning, TDiagnostic } from './diagnostics.js'; +import { TokenStream } from '../../../../../../../editor/common/codecs/utils/tokenStream.js'; +import { SimpleToken } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; +import { FrontMatterRecord } from '../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; +import { FrontMatterDecoder, TFrontMatterToken } from '../../../../../../../editor/common/codecs/frontMatterCodec/frontMatterDecoder.js'; + +/** + * Metadata defined in the prompt header. + */ +export interface IHeaderMetadata { + /** + * Tools metadata in the prompt header. + */ + tools?: PromptToolsMetadata; + + /** + * Description metadata in the prompt header. + */ + description?: PromptDescriptionMetadata; +} + +/** + * Prompt header holds all metadata records for a prompt. + */ +export class PromptHeader extends Disposable { + /** + * Underlying decoder for a Front Matter header. + */ + private readonly stream: FrontMatterDecoder; + + /** + * Metadata records. + */ + private readonly meta: IHeaderMetadata; + /** + * Metadata records. + */ + public get metadata(): Readonly { + return this.meta; + } + + /** + * List of all unique metadata record names. + */ + private readonly recordNames: Set; + + /** + * List of all issues found while parsing the prompt header. + */ + private readonly issues: TDiagnostic[]; + + /** + * List of all diagnostic issues found while parsing + * the prompt header. + */ + public get diagnostics(): readonly TDiagnostic[] { + return this.issues; + } + + constructor( + public readonly contentsToken: Text, + ) { + super(); + + this.issues = []; + this.meta = {}; + this.recordNames = new Set(); + + this.stream = this._register( + new FrontMatterDecoder( + new TokenStream(contentsToken.tokens), + ), + ); + this.stream.onData(this.onData.bind(this)); + this.stream.onError(this.onError.bind(this)); + } + + /** + * Process front matter tokens, converting them into + * well-known prompt metadata records. + */ + private onData(token: TFrontMatterToken): void { + // we currently expect only front matter 'records' for + // the prompt metadata, hence add diagnostics for all + // other tokens and ignore them + if ((token instanceof FrontMatterRecord) === false) { + // unless its a simple token, in which case we just ignore it + if (token instanceof SimpleToken) { + return; + } + + this.issues.push( + new PromptMetadataError( + token.range, + localize( + 'prompt.header.diagnostics.unexpected-token', + "Unexpected token '{0}'.", + token.text, + ), + ), + ); + + return; + } + + const recordName = token.nameToken.text; + + // if we already have a record with this name, + // add a warning diagnostic and ignore it + if (this.recordNames.has(recordName)) { + this.issues.push( + new PromptMetadataWarning( + token.range, + localize( + 'prompt.header.metadata.diagnostics.duplicate-record', + "Duplicate metadata record '{0}' will be ignored.", + recordName, + ), + ), + ); + + return; + } + + // if the record might be a "tools" metadata + // add it to the list of parsed metadata records + if (PromptToolsMetadata.isToolsRecord(token)) { + const toolsMetadata = new PromptToolsMetadata(token); + const { diagnostics } = toolsMetadata; + + this.issues.push(...diagnostics); + this.meta.tools = toolsMetadata; + this.recordNames.add(recordName); + + return; + } + + // if the record might be a "description" metadata + // add it to the list of parsed metadata records + if (PromptDescriptionMetadata.isDescriptionRecord(token)) { + const descriptionMetadata = new PromptDescriptionMetadata(token); + const { diagnostics } = descriptionMetadata; + + this.issues.push(...diagnostics); + this.meta.description = descriptionMetadata; + this.recordNames.add(recordName); + return; + } + + // all other records are currently not supported + this.issues.push( + new PromptMetadataWarning( + token.range, + localize( + 'prompt.header.metadata.diagnostics.unknown-record', + "Unknown metadata record '{0}' will be ignored.", + recordName, + ), + ), + ); + } + + /** + * Process errors from the underlying front matter decoder. + */ + private onError(error: Error): void { + this.issues.push( + new PromptMetadataError( + this.contentsToken.range, + localize( + 'prompt.header.diagnostics.parsing-error', + "Failed to parse prompt header: {0}", + error.message, + ), + ), + ); + } + + /** + * Promise that resolves when parsing process of + * the prompt header completes. + */ + public get settled(): Promise { + return this.stream.settled; + } + + /** + * Starts the parsing process of the prompt header. + */ + public start(): this { + this.stream.start(); + + return this; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts new file mode 100644 index 00000000000..7181acacbe9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptMetadataRecord } from './record.js'; +import { localize } from '../../../../../../../../nls.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError } from '../diagnostics.js'; +import { FrontMatterRecord, FrontMatterString, FrontMatterToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the `description` metadata record in the prompt header. + */ +const RECORD_NAME = 'description'; + +/** + * Prompt `description` metadata record inside the prompt header. + */ +export class PromptDescriptionMetadata extends PromptMetadataRecord { + /** + * Private field for tracking all diagnostic issues + * related to this metadata record. + */ + private readonly issues: PromptMetadataDiagnostic[]; + + /** + * List of all diagnostic issues related to this metadata record. + */ + public get diagnostics(): readonly PromptMetadataDiagnostic[] { + return this.issues; + } + + /** + * Value token reference of the record. + */ + private valueToken: FrontMatterString | undefined; + + /** + * Clean text value of the record. + */ + public get text(): string | null { + const { valueToken } = this; + + if (valueToken === undefined) { + return null; + } + + return valueToken.cleanText; + } + + constructor( + private readonly recordToken: FrontMatterRecord, + ) { + // sanity check on the name of the record + assert( + PromptDescriptionMetadata.isDescriptionRecord(recordToken), + `Record token must be 'description', got '${recordToken.nameToken.text}'.`, + ); + + super(recordToken.range); + + this.issues = []; + this.collectDiagnostics(); + } + + /** + * Validate the metadata record and collect all issues + * related to its content. + */ + private collectDiagnostics(): void { + const { valueToken } = this.recordToken; + + // validate that the record value is a string + if ((valueToken instanceof FrontMatterString) === false) { + this.issues.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.description.diagnostics.invalid-value-type', + "Value of the '{0}' metadata must be '{1}', got '{2}.", + RECORD_NAME, + 'string', + valueToken.valueTypeName, + ), + ), + ); + + return; + } + + this.valueToken = valueToken; + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `description`. + */ + public static isDescriptionRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === RECORD_NAME) { + return true; + } + + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts new file mode 100644 index 00000000000..4bce7f20e02 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/record.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../../../../../../editor/common/core/range.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; + +/** + * Abstract class for all metadata records in the prompt header. + */ +export abstract class PromptMetadataRecord { + /** + * List of diagnostic objects related to this metadata record. + */ + abstract readonly diagnostics: readonly PromptMetadataDiagnostic[]; + + constructor( + /** + * Full range of the metadata's record text in the prompt header. + */ + public readonly range: Range, + ) { } + + /** + * List of all `error` issue diagnostics. + */ + public get errorDiagnostics(): readonly PromptMetadataError[] { + return this.diagnostics + .filter((diagnostic) => { + return (diagnostic instanceof PromptMetadataError); + }); + } + + /** + * List of all `warning` issue diagnostics. + */ + public get warningDiagnostics(): readonly PromptMetadataWarning[] { + return this.diagnostics + .filter((diagnostic) => { + return (diagnostic instanceof PromptMetadataWarning); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts new file mode 100644 index 00000000000..ac75d971047 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptMetadataRecord } from './record.js'; +import { localize } from '../../../../../../../../nls.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; +import { FrontMatterArray, FrontMatterRecord, FrontMatterString, FrontMatterToken, FrontMatterValueToken } from '../../../../../../../../editor/common/codecs/frontMatterCodec/tokens/index.js'; + +/** + * Name of the `tools` metadata record in the prompt header. + */ +const TOOLS_NAME = 'tools'; + +/** + * Prompt `tools` metadata record inside the prompt header. + */ +export class PromptToolsMetadata extends PromptMetadataRecord { + /** + * Private field for tracking all diagnostic issues + * related to this metadata record. + */ + private readonly issues: PromptMetadataDiagnostic[]; + + /** + * List of all diagnostic issues related to this metadata record. + */ + public get diagnostics(): readonly PromptMetadataDiagnostic[] { + return this.issues; + } + + /** + * List of all valid tool names that were found in + * this metadata record. + */ + private validToolNames: Set; + + /** + * List of all valid tool names that were found in + * this metadata record. + */ + public get toolNames(): readonly string[] { + return [...this.validToolNames.values()]; + } + + constructor( + private readonly recordToken: FrontMatterRecord, + ) { + // sanity check on the name of the tools record + assert( + PromptToolsMetadata.isToolsRecord(recordToken), + `Record token must be a tools token, got '${recordToken.nameToken.text}'.`, + ); + + super(recordToken.range); + + this.issues = []; + this.validToolNames = new Set(); + this.collectDiagnostics(); + } + + /** + * Validate the metadata record and collect all issues + * related to its content. + */ + private collectDiagnostics(): void { + const { valueToken } = this.recordToken; + + // validate that the record value is an array + if ((valueToken instanceof FrontMatterArray) === false) { + this.issues.push( + new PromptMetadataError( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.invalid-value-type', + "Value of the '{0}' metadata must be '{1}', got '{2}.", + TOOLS_NAME, + 'array', + valueToken.valueTypeName, + ), + ), + ); + + return; + } + + const arrayValue: FrontMatterArray = valueToken; + + // validate that all array items + for (const item of arrayValue.items) { + this.validateToolName(item); + } + } + + /** + * Validate an individual provided value token that + * is used for a tool name. + */ + private validateToolName( + valueToken: FrontMatterValueToken, + ): void { + // tool name must be a string + if ((valueToken instanceof FrontMatterString) === false) { + this.issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.invalid-tool-name-type', + "Expected a tool name ({0}), got '{1}'.", + 'string', + valueToken.text, + ), + ), + ); + + return; + } + + const cleanToolName = valueToken.cleanText.trim(); + // the tool name should not be empty + if (cleanToolName.length === 0) { + this.issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.empty-tool-name', + "Tool name cannot be empty.", + ), + ), + ); + + return; + } + + // the tool name should not be duplicated + if (this.validToolNames.has(cleanToolName)) { + this.issues.push( + new PromptMetadataWarning( + valueToken.range, + localize( + 'prompt.header.metadata.tools.diagnostics.duplicate-tool-name', + "Duplicate tool name '{0}'.", + cleanToolName, + ), + ), + ); + + return; + } + + // collect all valid tool names + this.validToolNames.add(cleanToolName); + } + + /** + * Check if a provided front matter token is a metadata record + * with name equal to `tools`. + */ + public static isToolsRecord( + token: FrontMatterToken, + ): boolean { + if ((token instanceof FrontMatterRecord) === false) { + return false; + } + + if (token.nameToken.text === TOOLS_NAME) { + return true; + } + + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 096ada44af7..b9e8d297a7e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IRange, Range } from '../../../../../../editor/common/core/range.js'; +import { IHeaderMetadata } from './promptHeader/header.ts'; /** * A resolve error with a parent prompt URI, if any. @@ -48,6 +49,21 @@ export interface ITopError extends IResolveError { readonly localizedMessage: string; } +/** + * Metadata defined in the prompt header. + */ +export interface IPromptMetadata { + /** + * Tools metadata in the prompt header. + */ + tools?: readonly string[]; + + /** + * Description metadata in the prompt header. + */ + description?: string; +} + /** * Base interface for a generic prompt reference. */ @@ -132,13 +148,13 @@ interface IPromptReferenceBase extends IDisposable { /** * Direct references of the current reference. */ - references: readonly IPromptReference[]; + readonly references: readonly IPromptReference[]; /** * All references that the current reference may have, * including all possible nested child references. */ - allReferences: readonly IPromptReference[]; + readonly allReferences: readonly IPromptReference[]; /** * All *valid* references that the current reference may have, @@ -148,7 +164,18 @@ interface IPromptReferenceBase extends IDisposable { * without creating a circular reference loop or having any other * issues that would make the reference resolve logic to fail. */ - allValidReferences: readonly IPromptReference[]; + readonly allValidReferences: readonly IPromptReference[]; + + /** + * Entire associated `tools` metadata for this reference and + * all possible nested child references. + */ + readonly allToolsMetadata: readonly string[] | null; + + /** + * Metadata defined in the prompt header. + */ + readonly metadata: IPromptMetadata; /** * Returns a promise that resolves when the reference contents diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 000bc70a9c9..c910ffaf4c5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptPath, IPromptsService } from './types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { assert } from '../../../../../../base/common/assert.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; @@ -11,8 +10,13 @@ import { ITextModel } from '../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { ObjectCache } from '../../../../../../base/common/objectCache.js'; import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { IChatPromptSlashCommand, IPromptPath, IPromptsService, TPromptsStorage, TPromptsType } from './types.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; +import { localize } from '../../../../../../nls.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { basename } from '../../../../../../base/common/path.js'; /** * Provides prompt services. @@ -33,6 +37,7 @@ export class PromptsService extends Disposable implements IPromptsService { constructor( @IInstantiationService private readonly initService: IInstantiationService, @IUserDataProfileService private readonly userDataService: IUserDataProfileService, + @ILabelService private readonly labelService: ILabelService, ) { super(); @@ -83,45 +88,82 @@ export class PromptsService extends Disposable implements IPromptsService { return this.cache.get(model); } - public async listPromptFiles(): Promise { + public async listPromptFiles(type: TPromptsType): Promise { const userLocations = [this.userDataService.currentProfile.promptsHome]; const prompts = await Promise.all([ - this.fileLocator.listFilesIn(userLocations) - .then(withType('user')), - this.fileLocator.listFiles() - .then(withType('local')), + this.fileLocator.listFilesIn(userLocations, type) + .then(withType('user', type)), + this.fileLocator.listFiles(type) + .then(withType('local', type)), ]); return prompts.flat(); } - public getSourceFolders( - type: IPromptPath['type'], - ): readonly IPromptPath[] { + public getSourceFolders(type: TPromptsType): readonly IPromptPath[] { // sanity check to make sure we don't miss a new // prompt type that could be added in the future assert( - type === 'local' || type === 'user', + type === 'prompt' || type === 'instructions', `Unknown prompt type '${type}'.`, ); - const prompts = (type === 'user') - ? [this.userDataService.currentProfile.promptsHome] - : this.fileLocator.getConfigBasedSourceFolders(); + const result: IPromptPath[] = []; - return prompts.map(addType(type)); + for (const uri of this.fileLocator.getConfigBasedSourceFolders()) { + result.push({ uri, storage: 'local', type }); + } + const userHome = this.userDataService.currentProfile.promptsHome; + result.push({ uri: userHome, storage: 'user', type }); + + return result; + } + + public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { + if (command.match(/^prompt:[\w_\-\.]+/)) { + return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) }; + } + return undefined; + } + + public async resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise { + if (data.promptPath) { + return data.promptPath; + } + const files = await this.listPromptFiles('prompt'); + const command = data.command; + return files.find(file => getCommandName(file.uri.path) === command); + } + + public async findPromptSlashCommands(): Promise { + const promptFiles = await this.listPromptFiles('prompt'); + return promptFiles.map(promptPath => { + const command = getCommandName(promptPath.uri.path); + return { + command, + detail: localize('prompt.file.detail', 'Prompt file: {0}', this.labelService.getUriLabel(promptPath.uri, { relative: true })), + promptPath + }; + }); } } +function getCommandName(path: string) { + const name = basename(path, PROMPT_FILE_EXTENSION); + return `prompt:${name}`; +} + + /** * Utility to add a provided prompt `type` to a prompt URI. */ const addType = ( - type: 'local' | 'user', + storage: TPromptsStorage, + type: TPromptsType, ): (uri: URI) => IPromptPath => { return (uri) => { - return { uri, type: type }; + return { uri, storage, type }; }; }; @@ -129,10 +171,11 @@ const addType = ( * Utility to add a provided prompt `type` to a list of prompt URIs. */ const withType = ( - type: 'local' | 'user', + storage: TPromptsStorage, + type: TPromptsType, ): (uris: readonly URI[]) => (readonly IPromptPath[]) => { return (uris) => { return uris - .map(addType(type)); + .map(addType(storage, type)); }; }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts index ca6bec78b6d..0d92ac1991f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -15,11 +15,14 @@ import { createDecorator } from '../../../../../../platform/instantiation/common export const IPromptsService = createDecorator('IPromptsService'); /** -* Supported prompt types. -* - `local` means the prompt is a local file. -* - `user` means a "roam-able" prompt file (similar to snippets). -*/ -type TPromptsType = 'local' | 'user'; + * Where the prompt is stored. + */ +export type TPromptsStorage = 'local' | 'user'; + +/** + * What the prompt is used for. + */ +export type TPromptsType = 'instructions' | 'prompt'; /** * Represents a prompt path with its type. @@ -32,7 +35,12 @@ export interface IPromptPath { readonly uri: URI; /** - * Type of the prompt. + * Storage of the prompt. + */ + readonly storage: TPromptsStorage; + + /** + * Type */ readonly type: TPromptsType; } @@ -54,17 +62,33 @@ export interface IPromptsService extends IDisposable { /** * List all available prompt files. */ - listPromptFiles(): Promise; + listPromptFiles(type: TPromptsType): Promise; /** * Get a list of prompt source folders based on the provided prompt type. */ getSourceFolders(type: TPromptsType): readonly IPromptPath[]; + + /** + * Returns a prompt command if the command name. + * Undefined is returned if the name does not look like a file name of a prompt file. + */ + asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined; + + /** + * Gets the prompt file for a slash command. + */ + resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise; + + /** + * Returns a prompt command if the command name is valid. + */ + findPromptSlashCommands(): Promise; + } -/** - * Decoration CSS class modifiers. - */ -export enum CssClassModifiers { - Inactive = '.prompt-decoration-inactive', +export interface IChatPromptSlashCommand { + readonly command: string; + readonly detail: string; + readonly promptPath?: IPromptPath; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 975d45f56e2..a9a168abd9a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TPromptsType } from '../service/types.js'; import { URI } from '../../../../../../base/common/uri.js'; import { match } from '../../../../../../base/common/glob.js'; import { assert } from '../../../../../../base/common/assert.js'; @@ -13,7 +14,7 @@ import { PromptsConfig } from '../../../../../../platform/prompts/common/config. import { basename, dirname, extUri } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; +import { getPromptFileType, PROMPT_FILE_EXTENSION } from '../../../../../../platform/prompts/common/constants.js'; /** * Utility class to locate prompt files. @@ -30,11 +31,11 @@ export class PromptFilesLocator { * * @returns List of prompt files found in the workspace. */ - public async listFiles(): Promise { + public async listFiles(type: TPromptsType): Promise { const configuredLocations = PromptsConfig.promptSourceFolders(this.configService); const absoluteLocations = toAbsoluteLocations(configuredLocations, this.workspaceService); - return await this.listFilesIn(absoluteLocations); + return await this.listFilesIn(absoluteLocations, type); } /** @@ -47,8 +48,9 @@ export class PromptFilesLocator { */ public async listFilesIn( folders: readonly URI[], + type: TPromptsType, ): Promise { - return await this.findInstructionFiles(folders); + return await this.findFilesInLocations(folders, type); } /** @@ -112,9 +114,11 @@ export class PromptFilesLocator { * @param absoluteLocations List of prompt file source folders to search for prompt files in. Must be absolute paths. * @returns List of prompt files found in the provided source folders. */ - private async findInstructionFiles( + private async findFilesInLocations( absoluteLocations: readonly URI[], + type: TPromptsType, ): Promise { + // find all prompt files in the provided locations, then match // the found file paths against (possible) glob patterns const paths = new ResourceSet(); @@ -124,27 +128,34 @@ export class PromptFilesLocator { `Provided location must be an absolute path, got '${absoluteLocation.path}'.`, ); - // normalize the glob pattern to always end with "any prompt file" pattern - // unless the last part of the path is already a glob pattern itself; this is - // to handle the case when a user specifies a file glob pattern at the end, e.g., - // "my-folder/*.md" or "my-folder/*" already include the prompt files - const location = (isValidGlob(basename(absoluteLocation)) || absoluteLocation.path.endsWith(PROMPT_FILE_EXTENSION)) - ? absoluteLocation - : extUri.joinPath(absoluteLocation, `*${PROMPT_FILE_EXTENSION}`); - - // find all prompt files in entire file tree, starting from - // a first parent folder that does not contain a glob pattern - const promptFiles = await findAllPromptFiles( - firstNonGlobParent(location), - this.fileService, - ); - - // filter out found prompt files to only include those that match - // the original glob pattern specified in the settings (if any) - for (const file of promptFiles) { - if (match(location.path, file.path)) { + const nonGlobParent = firstNonGlobParent(absoluteLocation); + if (nonGlobParent === absoluteLocation) { + // the path does not contain a glob pattern, so we can + // just find all prompt files in the provided location + const promptFiles = await findFilesInLocation( + absoluteLocation, + type, + this.fileService, + ); + for (const file of promptFiles) { paths.add(file); } + } else { + // the path contains a glob pattern + // need to discuss whether to keep it or how to limit it (not documented yet) + const promptFiles = await findFilesInLocation( + nonGlobParent, + type, + this.fileService, + ); + + // filter out found prompt files to only include those that match + // the original glob pattern specified in the settings (if any) + for (const file of promptFiles) { + if (match(absoluteLocation.path, file.path)) { + paths.add(file); + } + } } } @@ -266,8 +277,9 @@ export const firstNonGlobParent = ( /** * Finds all `prompt files` in the provided location and all of its subfolders. */ -const findAllPromptFiles = async ( +const findFilesInLocation = async ( location: URI, + type: TPromptsType, fileService: IFileService, ): Promise => { const result: URI[] = []; @@ -275,7 +287,7 @@ const findAllPromptFiles = async ( try { const info = await fileService.resolve(location); - if (info.isFile && isPromptFile(info.resource)) { + if (info.isFile && getPromptFileType(info.resource) === type) { result.push(info.resource); return result; @@ -283,14 +295,14 @@ const findAllPromptFiles = async ( if (info.isDirectory && info.children) { for (const child of info.children) { - if (child.isFile && isPromptFile(child.resource)) { + if (child.isFile && getPromptFileType(child.resource) === type) { result.push(child.resource); continue; } if (child.isDirectory) { - const promptFiles = await findAllPromptFiles(child.resource, fileService); + const promptFiles = await findFilesInLocation(child.resource, type, fileService); result.push(...promptFiles); continue; diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index c5fa196451a..ba9a79c3f1a 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -7,80 +7,39 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; -import { localize } from '../../../../../nls.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { SaveReason } from '../../../../common/editor.js'; -import { GroupsOrder, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; -import { IToolInputProcessor } from './tools.js'; - -const codeInstructions = ` -The user is very smart and can understand how to apply your edits to their files, you just need to provide minimal hints. -Avoid repeating existing code, instead use comments to represent regions of unchanged code. The user prefers that you are as concise as possible. For example: -// ...existing code... -{ changed code } -// ...existing code... -{ changed code } -// ...existing code... - -Here is an example of how you should use format an edit to an existing Person class: -class Person { - // ...existing code... - age: number; - // ...existing code... - getAge() { - return this.age; - } -} -`; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; export const EditToolData: IToolData = { id: InternalEditToolId, - displayName: localize('chat.tools.editFile', "Edit File"), - modelDescription: `Edit a file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the "explanation" property first. ${codeInstructions}`, + displayName: '', // not used + modelDescription: '', // Not used source: { type: 'internal' }, - inputSchema: { - type: 'object', - properties: { - explanation: { - type: 'string', - description: 'A short explanation of the edit being made. Can be the same as the explanation you showed to the user.', - }, - filePath: { - type: 'string', - description: 'An absolute path to the file to edit, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.', - }, - code: { - type: 'string', - description: 'The code change to apply to the file. ' + codeInstructions - } - }, - required: ['explanation', 'filePath', 'code'] - } }; +export interface EditToolParams { + uri: UriComponents; + explanation: string; + code: string; +} + export class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, @ICodeMapperService private readonly codeMapperService: ICodeMapperService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, @ITextFileService private readonly textFileService: ITextFileService, @INotebookService private readonly notebookService: INotebookService, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, ) { } async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { @@ -89,26 +48,9 @@ export class EditTool implements IToolImpl { } const parameters = invocation.parameters as EditToolParams; - const fileUri = URI.revive(parameters.file); // TODO@roblourens do revive in MainThreadLanguageModelTools + const fileUri = URI.revive(parameters.uri); const uri = CellUri.parse(fileUri)?.notebook || fileUri; - if (!this.workspaceContextService.isInsideWorkspace(uri) && !this.notebookService.getNotebookTextModel(uri)) { - const groupsByLastActive = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); - const uriIsOpenInSomeEditor = groupsByLastActive.some((group) => { - return group.editors.some((editor) => { - return isEqual(editor.resource, uri); - }); - }); - - if (!uriIsOpenInSomeEditor) { - throw new Error(`File ${uri.fsPath} can't be edited because it's not inside the current workspace`); - } - } - - if (await this.ignoredFilesService.fileIsIgnored(uri, token)) { - throw new Error(`File ${uri.fsPath} can't be edited because it is configured to be ignored by Copilot`); - } - const model = this.chatService.getSession(invocation.context?.sessionId) as ChatModel; const request = model.getRequests().at(-1)!; @@ -158,7 +100,8 @@ export class EditTool implements IToolImpl { const result = await this.codeMapperService.mapCode({ codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }], location: 'tool', - chatRequestId: invocation.chatRequestId + chatRequestId: invocation.chatRequestId, + chatRequestModel: invocation.modelId, }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); @@ -217,31 +160,3 @@ export class EditTool implements IToolImpl { }; } } - -export interface EditToolParams { - file: UriComponents; - explanation: string; - code: string; -} - -export interface EditToolRawParams { - filePath: string; - explanation: string; - code: string; -} - -export class EditToolInputProcessor implements IToolInputProcessor { - processInput(input: EditToolRawParams): EditToolParams { - if (!input.filePath) { - // Tool name collision, or input wasn't properly validated upstream - return input as any; - } - const filePath = input.filePath; - // Runs in EH, will be mapped - return { - file: filePath.startsWith('untitled:') ? URI.parse(filePath) : URI.file(filePath), - explanation: input.explanation, - code: input.code, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index 4ecd8065216..518043f0760 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -193,16 +193,14 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri isProposedApiEnabled(extension.description, 'chatParticipantPrivate'); const tool: IToolData = { ...rawTool, - source: { type: 'extension', extensionId: extension.description.identifier, isExternalTool: !isBuiltinTool }, + source: { type: 'extension', label: extension.description.displayName ?? extension.description.name, extensionId: extension.description.identifier, isExternalTool: !isBuiltinTool }, inputSchema: rawTool.inputSchema, id: rawTool.name, icon, when: rawTool.when ? ContextKeyExpr.deserialize(rawTool.when) : undefined, requiresConfirmation: !isBuiltinTool, alwaysDisplayInputOutput: !isBuiltinTool, - supportsToolPicker: isBuiltinTool ? - false : - rawTool.canBeReferencedInPrompt + supportsToolPicker: rawTool.canBeReferencedInPrompt }; const disposable = languageModelToolsService.registerToolData(tool); this._registrationDisposables.set(toToolKey(extension.description.identifier, rawTool.name), disposable); diff --git a/src/vs/workbench/contrib/chat/common/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/tools.ts index eac67b132eb..1ca0556cc94 100644 --- a/src/vs/workbench/contrib/chat/common/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/tools.ts @@ -25,8 +25,4 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo } } -export interface IToolInputProcessor { - processInput(input: any): any; -} - export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 7a565b05fd7..954291a13fb 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -101,7 +101,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index 5207cb81433..eac347edaac 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -85,7 +85,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index b8b5830eb3f..6c53d43dcbf 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -108,7 +108,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined }, { requestId: undefined, @@ -162,7 +163,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 0f10279e625..f6a351338de 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -85,7 +85,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 21acec106cf..6d8091a23fa 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -19,9 +19,10 @@ import { IChatService } from '../../common/chatService.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { ChatMode, ChatAgentLocation } from '../../common/constants.js'; -import { ILanguageModelToolsService, IToolData } from '../../common/languageModelToolsService.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; +import { IPromptsService } from '../../common/promptSyntax/service/types.js'; import { MockChatService } from './mockChatService.js'; -import { MockChatVariablesService } from './mockChatVariables.js'; +import { MockPromptsService } from './mockPromptsService.js'; suite('ChatRequestParser', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -29,7 +30,7 @@ suite('ChatRequestParser', () => { let instantiationService: TestInstantiationService; let parser: ChatRequestParser; - let toolsService: MockObject; + let variableService: MockObject; setup(async () => { instantiationService = testDisposables.add(new TestInstantiationService()); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); @@ -37,11 +38,14 @@ suite('ChatRequestParser', () => { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); - instantiationService.stub(IChatVariablesService, new MockChatVariablesService()); instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IPromptsService, testDisposables.add(new MockPromptsService())); - toolsService = mockObject()({}); - instantiationService.stub(ILanguageModelToolsService, toolsService as any); + variableService = mockObject()(); + variableService.getDynamicVariables.returns([]); + variableService.getSelectedTools.returns([]); + + instantiationService.stub(IChatVariablesService, variableService as any); }); test('plain text', async () => { @@ -198,8 +202,10 @@ suite('ChatRequestParser', () => { agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); - toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); - toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); + variableService.getSelectedTools.returns([ + { id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }, + { id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } + ] satisfies IToolData[]); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole'); @@ -211,8 +217,10 @@ suite('ChatRequestParser', () => { agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); instantiationService.stub(IChatAgentService, agentsService as any); - toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); - toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData); + variableService.getSelectedTools.returns([ + { id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }, + { id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } + ] satisfies IToolData[]); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index bc817a7b8df..796050f1a23 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChatRequestVariableData, IChatRequestVariableEntry } from '../../common/chatModel.js'; -import { IParsedChatRequest } from '../../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; +import { IToolData } from '../../common/languageModelToolsService.js'; export class MockChatVariablesService implements IChatVariablesService { _serviceBrand: undefined; @@ -14,9 +13,7 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableData { - return { - variables: [] - }; + getSelectedTools(sessionId: string): readonly IToolData[] { + return []; } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts new file mode 100644 index 00000000000..9ad92c1803d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModel } from '../../../../../editor/common/model.js'; +import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js'; +import { TextModelPromptParser } from '../../common/promptSyntax/parsers/textModelPromptParser.js'; +import { IChatPromptSlashCommand, IPromptPath, IPromptsService, TPromptsType } from '../../common/promptSyntax/service/types.js'; + +export class MockPromptsService implements IPromptsService { + _serviceBrand: undefined; + getSyntaxParserFor(model: ITextModel): TextModelPromptParser & { disposed: false } { + throw new Error('Method not implemented.'); + } + listPromptFiles(type: TPromptsType): Promise { + throw new Error('Method not implemented.'); + } + getSourceFolders(type: TPromptsType): readonly IPromptPath[] { + throw new Error('Method not implemented.'); + } + public asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined { + if (name.endsWith(PROMPT_FILE_EXTENSION)) { + const command = `prompt:${name.substring(0, -PROMPT_FILE_EXTENSION.length)}`; + return { + command, detail: name, + }; + } + return undefined; + } + resolvePromptSlashCommand(data: IChatPromptSlashCommand): Promise { + throw new Error('Method not implemented.'); + } + findPromptSlashCommands(): Promise { + throw new Error('Method not implemented.'); + } + dispose(): void { + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts index f1b6552fe5b..46573d18f65 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts @@ -18,8 +18,8 @@ import { TestSimpleDecoder } from '../../../../../../../editor/test/common/codec import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; import { Colon, Dash, Space, Tab, VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; -import { MarkdownExtensionsDecoder } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.js'; import { FrontMatterHeader } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { MarkdownExtensionsDecoder } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.js'; import { FrontMatterMarker, TMarkerToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; /** diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index 4ab256e62d1..32f0d97749e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -8,6 +8,7 @@ import { createURI } from '../testUtils/createUri.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { ExpectedReference } from '../testUtils/expectedReference.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../../../../editor/common/model.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; @@ -17,6 +18,7 @@ import { randomBoolean } from '../../../../../../../base/test/common/testUtils.j import { FileService } from '../../../../../../../platform/files/common/fileService.js'; import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { ExpectedDiagnosticWarning, TExpectedDiagnostic } from '../testUtils/expectedDiagnostic.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; @@ -95,6 +97,39 @@ class TextModelPromptParserTest extends Disposable { `[${this.model.uri}] Unexpected number of references.`, ); } + + /** + * Validate list of diagnostic objects of the prompt header. + */ + public async validateHeaderDiagnostics( + expectedDiagnostics: readonly TExpectedDiagnostic[], + ) { + await this.parser.allSettled(); + + const { header } = this.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + const { diagnostics } = header; + + for (let i = 0; i < expectedDiagnostics.length; i++) { + const diagnostic = diagnostics[i]; + + assertDefined( + diagnostic, + `Expected diagnostic #${i} be ${expectedDiagnostics[i]}, got 'undefined'.`, + ); + + expectedDiagnostics[i].validateEqual(diagnostic); + } + + assert.strictEqual( + expectedDiagnostics.length, + diagnostics.length, + `Expected '${expectedDiagnostics.length}' diagnostic objects, got '${diagnostics.length}'.`, + ); + } } suite('TextModelPromptParser', () => { @@ -124,7 +159,7 @@ suite('TextModelPromptParser', () => { ); }; - test('core logic #1', async () => { + test('• core logic #1', async () => { const test = createTest( createURI('/foo/bar.md'), [ @@ -135,11 +170,11 @@ suite('TextModelPromptParser', () => { /* 05 */"Sometimes, the best code is the one you never have to write.", /* 06 */"A lone kangaroo once hopped into the local cafe, seeking free Wi-Fi.", /* 07 */"Critical #file:./folder/binary.file thinking is like coffee; best served strong [md link](/etc/hosts/random-file.txt) and without sugar.", - /* 08 */"Music is the mind’s way of doodling in the air.", + /* 08 */"Music is the mind's way of doodling in the air.", /* 09 */"Stargazing is just turning your eyes into cosmic explorers.", /* 10 */"Never trust a balloon salesman who hates birthdays.", /* 11 */"Running backward can be surprisingly enlightening.", - /* 12 */"There’s an art to whispering loudly.", + /* 12 */"There's an art to whispering loudly.", ], ); @@ -174,7 +209,7 @@ suite('TextModelPromptParser', () => { ]); }); - test('core logic #2', async () => { + test('• core logic #2', async () => { const test = createTest( createURI('/absolute/folder/and/a/filename.txt'), [ @@ -236,7 +271,142 @@ suite('TextModelPromptParser', () => { ]); }); - test('gets disposed with the model', async () => { + suite('• header', () => { + test('• has correct metadata', async function () { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */" something: true", /* unknown metadata record */ + /* 03 */" tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', 'tool_name2' ]\t\t", + /* 04 */" tools: [ 'tool_name3', \"tool_name4\" ]", /* duplicate `tools` record is ignored */ + /* 05 */" tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 06 */"---", + /* 07 */"The cactus on my desk has a thriving Instagram account.", + /* 08 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 09 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 10 */"Lunar rainbows only appear when you sing in falsetto.", + /* 11 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 8, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools } = metadata; + assertDefined( + tools, + 'Tools metadata must be present.', + ); + + assert.strictEqual( + tools.length, + 2, + `Prompt header tools metadata must have 2 tool names, got '[${tools.join(', ')}]'.`, + ); + + assert.deepStrictEqual( + tools, + ['tool_name1', 'tool_name2'], + `Prompt header must have correct tools metadata.`, + ); + }); + + test('• has correct diagnostics', async function () { + const test = createTest( + createURI('/absolute/folder/and/a/filename.txt'), + [ + /* 01 */"---", + /* 02 */" something: true", /* unknown metadata record */ + /* 03 */"tools: [ 'tool_name1', \"tool_name2\", 'tool_name1', true, false, '', ,'tool_name2' ] ", + /* 04 */" tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ + /* 05 */"tools: 'tool_name5'", /* duplicate `tools` record with invalid value is ignored */ + /* 06 */"---", + /* 07 */"The cactus on my desk has a thriving Instagram account.", + /* 08 */"Midnight snacks are the secret to eternal [text](./foo-bar-baz/another-file.ts) happiness.", + /* 09 */"In an alternate universe, pigeons deliver sushi by drone.", + /* 10 */"Lunar rainbows only appear when you sing in falsetto.", + /* 11 */"Carrots have secret telepathic abilities, but only on Tuesdays.", + ], + ); + + await test.validateReferences([ + new ExpectedReference({ + uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + text: '[text](./foo-bar-baz/another-file.ts)', + path: './foo-bar-baz/another-file.ts', + startLine: 8, + startColumn: 43, + pathStartColumn: 50, + childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + }), + ]); + + const { header, metadata } = test.parser; + assertDefined( + header, + 'Prompt header must be defined.', + ); + + const { tools } = metadata; + assertDefined( + tools, + 'Tools metadata must be defined.', + ); + + await test.validateHeaderDiagnostics([ + new ExpectedDiagnosticWarning( + new Range(2, 2, 2, 2 + 15), + 'Unknown metadata record \'something\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 38, 3, 38 + 12), + 'Duplicate tool name \'tool_name1\'.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 52, 3, 52 + 4), + 'Expected a tool name (string), got \'true\'.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 58, 3, 58 + 5), + 'Expected a tool name (string), got \'false\'.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 65, 3, 65 + 2), + 'Tool name cannot be empty.', + ), + new ExpectedDiagnosticWarning( + new Range(3, 70, 3, 70 + 12), + 'Duplicate tool name \'tool_name2\'.', + ), + new ExpectedDiagnosticWarning( + new Range(4, 3, 4, 3 + 37), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + new ExpectedDiagnosticWarning( + new Range(5, 1, 5, 1 + 19), + 'Duplicate metadata record \'tools\' will be ignored.', + ), + ]); + }); + }); + + test('• gets disposed with the model', async () => { const test = createTest( createURI('/some/path/file.prompt.md'), [ @@ -257,7 +427,7 @@ suite('TextModelPromptParser', () => { ); }); - test('toString() implementation', async () => { + test('• toString() implementation', async () => { const modelUri = createURI('/Users/legomushroom/repos/prompt-snippets/README.md'); const test = createTest( modelUri, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index de647abed93..7b9a8781803 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -19,15 +19,17 @@ import { ILogService, NullLogService } from '../../../../../../platform/log/comm import { TErrorCondition } from '../../../common/promptSyntax/parsers/basePromptParser.js'; import { FileReference } from '../../../common/promptSyntax/codecs/tokens/fileReference.js'; import { FilePromptParser } from '../../../common/promptSyntax/parsers/filePromptParser.js'; -import { waitRandom, randomBoolean } from '../../../../../../base/test/common/testUtils.js'; +import { waitRandom, randomBoolean, wait } from '../../../../../../base/test/common/testUtils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js'; import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { IFileContentsProviderOptions } from '../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { NotPromptFile, RecursiveReference, OpenFailed, FolderReference } from '../../../common/promptFileReferenceErrors.js'; -import { IFileContentsProviderOptions } from '../../../common/promptSyntax/contentProviders/filePromptContentsProvider.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; /** * Represents a file reference with an expected @@ -41,10 +43,17 @@ class ExpectedReference { constructor( dirname: URI, - public readonly lineToken: FileReference, + public readonly linkToken: FileReference | MarkdownLink, public readonly errorCondition?: TErrorCondition, ) { - this.uri = extUri.resolvePath(dirname, lineToken.path); + this.uri = extUri.resolvePath(dirname, linkToken.path); + } + + /** + * Range of the underlying file reference token. + */ + public get range(): Range { + return this.linkToken.range; } /** @@ -78,10 +87,15 @@ class TestPromptFileReference extends Disposable { */ public async run( options: Partial = {}, - ) { + ): Promise { // create the files structure on the disk await (this.initService.createInstance(MockFilesystem, this.fileStructure)).mock(); + // wait for the filesystem event to settle before proceeding + // this is temporary workaround and should be fixed once we + // improve behavior of the `allSettled()` method + await wait(50); + // randomly test with and without delay to ensure that the file // reference resolution is not susceptible to race conditions if (randomBoolean()) { @@ -107,6 +121,26 @@ class TestPromptFileReference extends Disposable { const expectedReference = this.expectedReferences[i]; const resolvedReference = resolvedReferences[i]; + if (expectedReference.linkToken instanceof MarkdownLink) { + assert( + resolvedReference?.subtype === 'markdown', + [ + `Expected ${i}th resolved reference to be a markdown link`, + `got '${resolvedReference}'.`, + ].join(', '), + ); + } + + if (expectedReference.linkToken instanceof FileReference) { + assert( + resolvedReference?.subtype === 'prompt', + [ + `Expected ${i}th resolved reference to be a #file: link`, + `got '${resolvedReference}'.`, + ].join(', '), + ); + } + assert( (resolvedReference) && (resolvedReference.uri.toString() === expectedReference.uri.toString()), @@ -116,6 +150,15 @@ class TestPromptFileReference extends Disposable { ].join(', '), ); + assert( + (resolvedReference) && + (resolvedReference.range.equalsRange(expectedReference.range)), + [ + `Expected ${i}th resolved reference range to be '${expectedReference.range}'`, + `got '${resolvedReference?.range}'.`, + ].join(', '), + ); + if (expectedReference.errorCondition === undefined) { assert( resolvedReference.errorCondition === undefined, @@ -142,8 +185,10 @@ class TestPromptFileReference extends Disposable { [ `\nExpected(${this.expectedReferences.length}): [\n ${this.expectedReferences.join('\n ')}\n]`, `Received(${resolvedReferences.length}): [\n ${resolvedReferences.join('\n ')}\n]`, - ].join('\n') + ].join('\n'), ); + + return rootReference; } } @@ -239,7 +284,7 @@ suite('PromptFileReference (Unix)', function () { children: [ { name: 'another-file.prompt.md', - contents: `[](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, + contents: `[caption](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`, }, { name: 'one_more_file_just_in_case.prompt.md', @@ -267,10 +312,9 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), - createTestFileReference( - `./some-other-folder/non-existing-folder`, - 2, - 1, + new MarkdownLink( + 2, 1, + '[]', '(./some-other-folder/non-existing-folder)', ), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), @@ -287,7 +331,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), - createTestFileReference('.', 1, 1), + new MarkdownLink( + 1, 1, + '[caption]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), new FolderReference( URI.joinPath(rootUri, './folder1/some-other-folder'), 'This folder is not a prompt file!', @@ -295,7 +342,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), - createTestFileReference('../file.txt', 2, 35), + new MarkdownLink( + 2, 34, + '[#file:file.txt]', '(../file.txt)', + ), new NotPromptFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), 'Ughh oh, that is not a prompt file!', @@ -303,14 +353,17 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( rootUri, - createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), + new MarkdownLink( + 3, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), - 'Failed to open non-existring prompt snippets file', + 'Failed to open non-existing prompt snippets file', ), ), new ExpectedReference( @@ -323,7 +376,11 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), - createTestFileReference('../../folder1', 5, 48), + // createTestFileReference('../../folder1', 5, 48), + new MarkdownLink( + 5, 48, + '[]', '(../../folder1/)', + ), new FolderReference( URI.joinPath(rootUri, './folder1'), 'Uggh ohh!', @@ -407,14 +464,13 @@ suite('PromptFileReference (Unix)', function () { [ new ExpectedReference( rootUri, - createTestFileReference('folder1/file3.prompt.md', 2, 9), + createTestFileReference('folder1/file3.prompt.md', 2, 14), ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), - createTestFileReference( - `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, - 3, - 23, + new MarkdownLink( + 3, 26, + '[another-file.prompt.md]', `(${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md)`, ), ), /** @@ -474,7 +530,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( rootUri, - createTestFileReference('./file1.md', 6, 2), + new MarkdownLink( + 6, 2, + '[some (snippet!) #name))]', '(./file1.md)', + ), new NotPromptFile( URI.joinPath(rootUri, './file1.md'), 'Uggh oh!', @@ -562,10 +621,9 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1'), - createTestFileReference( - `./some-other-folder/non-existing-folder`, - 2, - 1, + new MarkdownLink( + 2, 1, + '[]', '(./some-other-folder/non-existing-folder)', ), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), @@ -582,7 +640,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), - createTestFileReference('.', 1, 1), + new MarkdownLink( + 1, 1, + '[]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), new FolderReference( URI.joinPath(rootUri, './folder1/some-other-folder'), 'This folder is not a prompt file!', @@ -590,7 +651,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), - createTestFileReference('../file.txt', 2, 35), + new MarkdownLink( + 2, 34, + '[#file:file.txt]', '(../file.txt)', + ), new NotPromptFile( URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), 'Ughh oh, that is not a prompt file!', @@ -598,14 +662,17 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( rootUri, - createTestFileReference('./folder1/some-other-folder/file4.prompt.md', 3, 14), + new MarkdownLink( + 3, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), ), new ExpectedReference( URI.joinPath(rootUri, './folder1/some-other-folder'), createTestFileReference('./some-non-existing/file.prompt.md', 1, 30), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), - 'Failed to open non-existring prompt snippets file', + 'Failed to open non-existing prompt snippets file', ), ), new ExpectedReference( @@ -618,7 +685,10 @@ suite('PromptFileReference (Unix)', function () { ), new ExpectedReference( URI.joinPath(rootUri, './some-other-folder/folder1'), - createTestFileReference('../../folder1', 5, 48), + new MarkdownLink( + 5, 48, + '[]', '(../../folder1/)', + ), new FolderReference( URI.joinPath(rootUri, './folder1'), 'Uggh ohh!', @@ -630,4 +700,222 @@ suite('PromptFileReference (Unix)', function () { await test.run({ allowNonPromptFiles: true }); }); }); + + suite('• metadata', () => { + test('• tools', async function () { + if (isWindows) { + this.skip(); + } + + const rootFolderName = 'resolves-nested-file-references'; + const rootFolder = `/${rootFolderName}`; + const rootUri = URI.file(rootFolder); + + const test = testDisposables.add(instantiationService.createInstance(TestPromptFileReference, + /** + * The file structure to be created on the disk for the test. + */ + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: 'file2.prompt.md', + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\']', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + ' ', + ], + }, + { + name: 'folder1', + children: [ + { + name: 'file3.prompt.md', + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents`, + ' some more\t content', + ], + }, + { + name: 'some-other-folder', + children: [ + { + name: 'file4.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + name: 'file.txt', + contents: 'contents of a non-prompt-snippet file', + }, + { + name: 'yetAnotherFolder🤭', + children: [ + { + name: 'another-file.prompt.md', + contents: [ + '---', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)', + ], + }, + { + name: 'one_more_file_just_in_case.prompt.md', + contents: 'one_more_file_just_in_case.prompt.md contents', + }, + ], + }, + ], + }, + ], + }, + ], + }], + /** + * The root file path to start the resolve process from. + */ + URI.file(`/${rootFolderName}/file2.prompt.md`), + /** + * The expected references to be resolved. + */ + [ + new ExpectedReference( + rootUri, + createTestFileReference('folder1/file3.prompt.md', 6, 14), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + new MarkdownLink( + 5, 1, + '[]', '(./some-other-folder/non-existing-folder)', + ), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'), + 'Reference to non-existing file cannot be opened.', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1'), + createTestFileReference( + `/${rootFolderName}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md`, + 6, + 26, + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + new MarkdownLink( + 4, 1, + '[]', `(/${rootFolderName}/folder1/some-other-folder)`, + ), + new FolderReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + 'This folder is not a prompt file!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'), + new MarkdownLink( + 5, 34, + '[#file:file.txt]', '(../file.txt)', + ), + new NotPromptFile( + URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'), + 'Ughh oh, that is not a prompt file!', + ), + ), + new ExpectedReference( + rootUri, + new MarkdownLink( + 7, 14, + '[file4.prompt.md]', '(./folder1/some-other-folder/file4.prompt.md)', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-existing/file.prompt.md', 5, 30), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'), + 'Failed to open non-existing prompt snippets file', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './folder1/some-other-folder'), + createTestFileReference('./some-non-prompt-file.md', 9, 13), + new OpenFailed( + URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), + 'Oh no!', + ), + ), + new ExpectedReference( + URI.joinPath(rootUri, './some-other-folder/folder1'), + new MarkdownLink( + 9, 48, + '[]', '(../../folder1/)', + ), + new FolderReference( + URI.joinPath(rootUri, './folder1'), + 'Uggh ohh!', + ), + ), + ] + )); + + const rootReference = await test.run(); + + const { metadata, allToolsMetadata } = rootReference; + const { tools, description } = metadata; + + assert.deepStrictEqual( + tools, + ['my-tool1'], + 'Must have correct tools metadata', + ); + + assert.deepStrictEqual( + description, + 'Root prompt description.', + 'Must have correct description metadata', + ); + + assertDefined( + allToolsMetadata, + 'All tools metadata must to be defined.', + ); + assert.deepStrictEqual( + allToolsMetadata, + ['my-tool1', 'my-tool3', 'my-tool2'], + 'Must have correct all tools metadata', + ); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts new file mode 100644 index 00000000000..5c39b6059dd --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedDiagnostic.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { assertNever } from '../../../../../../../base/common/assert.js'; +import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning, TDiagnostic } from '../../../../common/promptSyntax/parsers/promptHeader/diagnostics.js'; + +/** + * Base class for all expected diagnostics used in the unit tests. + */ +abstract class ExpectedDiagnostic extends PromptMetadataDiagnostic { + /** + * Validate that the provided diagnostic is equal to this object. + */ + public validateEqual(other: TDiagnostic) { + this.validateTypesEqual(other); + + assert.strictEqual( + this.message, + other.message, + `Expected message '${this.message}', got '${other.message}'.`, + ); + + assert( + this.range + .equalsRange(other.range), + `Expected range '${this.range}', got '${other.range}'.`, + ); + } + + /** + * Validate that the provided diagnostic is of the same + * diagnostic type as this object. + */ + private validateTypesEqual(other: TDiagnostic) { + if (other instanceof PromptMetadataWarning) { + assert( + this instanceof ExpectedDiagnosticWarning, + `Expected a warning diagnostic object, got '${other}'.`, + ); + + return; + } + + if (other instanceof PromptMetadataError) { + assert( + this instanceof ExpectedDiagnosticError, + `Expected a error diagnostic object, got '${other}'.`, + ); + + return; + } + + assertNever( + other, + `Unknown diagnostic type '${other}'.`, + ); + } +} + +/** + * Expected warning diagnostic object for testing purposes. + */ +export class ExpectedDiagnosticWarning extends ExpectedDiagnostic { + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `expected-diagnostic/warning(${this.message})${this.range}`; + } +} + +/** + * Expected error diagnostic object for testing purposes. + */ +export class ExpectedDiagnosticError extends ExpectedDiagnostic { + /** + * Returns a string representation of this object. + */ + public override toString(): string { + return `expected-diagnostic/error(${this.message})${this.range}`; + } +} + +/** + * Type for any expected diagnostic object. + */ +export type TExpectedDiagnostic = ExpectedDiagnosticWarning | ExpectedDiagnosticError; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts index 80db5124e43..b55d49e753b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts @@ -19,7 +19,7 @@ interface IMockFilesystemNode { * Represents a `file` node. */ export interface IMockFile extends IMockFilesystemNode { - contents: string; + contents: string | readonly string[]; } /** @@ -90,7 +90,11 @@ export class MockFilesystem { `File '${folderUri.path}' already exists.`, ); - await this.fileService.writeFile(childUri, VSBuffer.fromString(child.contents)); + const contents: string = (typeof child.contents === 'string') + ? child.contents + : child.contents.join('\n'); + + await this.fileService.writeFile(childUri, VSBuffer.fromString(contents)); resolvedChildren.push({ ...child, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 4342e42b78a..5a0920bd545 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -109,7 +109,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator(undefined, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -123,7 +123,7 @@ suite('PromptFilesLocator', () => { }, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - await locator.listFiles(), + await locator.listFiles('prompt'), [], 'No prompts must be found.', ); @@ -136,7 +136,7 @@ suite('PromptFilesLocator', () => { ], EMPTY_WORKSPACE, []); assert.deepStrictEqual( - await locator.listFiles(), + await locator.listFiles('prompt'), [], 'No prompts must be found.', ); @@ -146,7 +146,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator(null, EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -157,7 +157,7 @@ suite('PromptFilesLocator', () => { const locator = await createPromptsLocator('/etc/hosts/prompts', EMPTY_WORKSPACE, []); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [], 'No prompts must be found.', @@ -210,7 +210,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -287,7 +287,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -447,7 +447,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -531,7 +531,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -691,7 +691,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -771,7 +771,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/my.prompt.md').fsPath, @@ -931,7 +931,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/deps/text/nested/specific.prompt.md').fsPath, @@ -1016,7 +1016,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/my.prompt.md').fsPath, @@ -1103,7 +1103,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -1224,7 +1224,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md').path, @@ -1345,7 +1345,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/.github/prompts/default.prompt.md').fsPath, @@ -1469,7 +1469,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/prompts/test.prompt.md').path, @@ -1592,7 +1592,7 @@ suite('PromptFilesLocator', () => { ]); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ // all of these are due to the `.github/prompts` setting @@ -1707,7 +1707,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -1913,7 +1913,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -2038,7 +2038,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, @@ -2274,7 +2274,7 @@ suite('PromptFilesLocator', () => { ); assert.deepStrictEqual( - (await locator.listFiles()) + (await locator.listFiles('prompt')) .map((file) => file.fsPath), [ createURI('/Users/legomushroom/repos/vscode/gen/text/my.prompt.md').fsPath, diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index e8aedfe3cc5..3801ee2391e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import './emptyTextEditorHint.css'; -import * as dom from '../../../../../base/browser/dom.js'; -import { DisposableStore, dispose, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { $, addDisposableListener, getActiveWindow } from '../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../../editor/browser/editorBrowser.js'; import { localize } from '../../../../../nls.js'; import { ChangeLanguageAction } from '../../../../browser/parts/editor/editorStatus.js'; @@ -24,80 +24,60 @@ import { ApplyFileSnippetAction } from '../../../snippets/browser/commands/fileT import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; -import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { KeybindingLabel } from '../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { OS } from '../../../../../base/common/platform.js'; import { status } from '../../../../../base/browser/ui/aria/aria.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { LOG_MODE_ID, OUTPUT_MODE_ID } from '../../../../services/output/common/output.js'; import { SEARCH_RESULT_LANGUAGE_ID } from '../../../../services/search/common/search.js'; -import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; - -const $ = dom.$; - -export interface IEmptyTextEditorHintOptions { - readonly clickable?: boolean; -} +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; export const emptyTextEditorHintSetting = 'workbench.editor.empty.hint'; -export class EmptyTextEditorHintContribution implements IEditorContribution { +export class EmptyTextEditorHintContribution extends Disposable implements IEditorContribution { - public static readonly ID = 'editor.contrib.emptyTextEditorHint'; + static readonly ID = 'editor.contrib.emptyTextEditorHint'; - protected toDispose: IDisposable[]; private textHintContentWidget: EmptyTextEditorHintContentWidget | undefined; constructor( protected readonly editor: ICodeEditor, - @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, - @ICommandService private readonly commandService: ICommandService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @IHoverService protected readonly hoverService: IHoverService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInlineChatSessionService private readonly inlineChatSessionService: IInlineChatSessionService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IProductService protected readonly productService: IProductService, - @IContextMenuService private readonly contextMenuService: IContextMenuService + @IInstantiationService private readonly instantiationService: IInstantiationService ) { - this.toDispose = []; - this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelContent(() => this.update())); - this.toDispose.push(this.chatAgentService.onDidChangeAgents(() => this.update())); - this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); - this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + super(); + + this._register(this.editor.onDidChangeModel(() => this.update())); + this._register(this.editor.onDidChangeModelLanguage(() => this.update())); + this._register(this.editor.onDidChangeModelContent(() => this.update())); + this._register(this.chatAgentService.onDidChangeAgents(() => this.update())); + this._register(this.editor.onDidChangeModelDecorations(() => this.update())); + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.readOnly)) { this.update(); } })); - this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(emptyTextEditorHintSetting)) { this.update(); } })); - this.toDispose.push(inlineChatSessionService.onWillStartSession(editor => { + this._register(inlineChatSessionService.onWillStartSession(editor => { if (this.editor === editor) { this.textHintContentWidget?.dispose(); } })); - this.toDispose.push(inlineChatSessionService.onDidEndSession(e => { + this._register(inlineChatSessionService.onDidEndSession(e => { if (this.editor === e.editor) { this.update(); } })); } - protected _getOptions(): IEmptyTextEditorHintOptions { - return { clickable: true }; - } - - protected _shouldRenderHint() { + protected shouldRenderHint() { const configValue = this.configurationService.getValue(emptyTextEditorHintSetting); if (configValue === 'hidden') { return false; @@ -137,63 +117,49 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { } protected update(): void { - const shouldRenderHint = this._shouldRenderHint(); + const shouldRenderHint = this.shouldRenderHint(); if (shouldRenderHint && !this.textHintContentWidget) { - this.textHintContentWidget = new EmptyTextEditorHintContentWidget( - this.editor, - this._getOptions(), - this.editorGroupsService, - this.commandService, - this.configurationService, - this.hoverService, - this.keybindingService, - this.chatAgentService, - this.telemetryService, - this.productService, - this.contextMenuService - ); + this.textHintContentWidget = this.instantiationService.createInstance(EmptyTextEditorHintContentWidget, this.editor); } else if (!shouldRenderHint && this.textHintContentWidget) { this.textHintContentWidget.dispose(); this.textHintContentWidget = undefined; } } - dispose(): void { - dispose(this.toDispose); + override dispose(): void { + super.dispose(); + this.textHintContentWidget?.dispose(); } } -class EmptyTextEditorHintContentWidget implements IContentWidget { +class EmptyTextEditorHintContentWidget extends Disposable implements IContentWidget { private static readonly ID = 'editor.widget.emptyHint'; private domNode: HTMLElement | undefined; - private readonly toDispose: DisposableStore; private isVisible = false; private ariaLabel: string = ''; constructor( private readonly editor: ICodeEditor, - private readonly options: IEmptyTextEditorHintOptions, - private readonly editorGroupsService: IEditorGroupsService, - private readonly commandService: ICommandService, - private readonly configurationService: IConfigurationService, - private readonly hoverService: IHoverService, - private readonly keybindingService: IKeybindingService, - private readonly chatAgentService: IChatAgentService, - private readonly telemetryService: ITelemetryService, - private readonly productService: IProductService, - private readonly contextMenuService: IContextMenuService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { - this.toDispose = new DisposableStore(); - this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + super(); + + this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (this.domNode && e.hasChanged(EditorOption.fontInfo)) { this.editor.applyFontInfo(this.domNode); } })); const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500); - this.toDispose.add(onDidFocusEditorText(() => { + this._register(onDidFocusEditorText(() => { if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EmptyEditorHint)) { status(this.ariaLabel); } @@ -205,7 +171,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } - private _disableHint(e?: MouseEvent) { + private disableHint(e?: MouseEvent) { const disableHint = () => { this.configurationService.updateValue(emptyTextEditorHintSetting, 'hidden'); this.dispose(); @@ -218,7 +184,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } this.contextMenuService.showContextMenu({ - getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, + getAnchor: () => { return new StandardMouseEvent(getActiveWindow(), e); }, getActions: () => { return [{ id: 'workench.action.disableEmptyEditorHint', @@ -235,112 +201,39 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { }); } - private _getHintInlineChat(providers: IChatAgent[]) { - const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; - - const inlineChatId = 'inlineChat.start'; - let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; - - const handleClick = () => { - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: 'inlineChat.hintAction', - from: 'hint' - }); - this.commandService.executeCommand(inlineChatId, { from: 'hint' }); - }; + private getHint() { + const hasInlineChatProvider = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)).length > 0; const hintHandler: IContentActionHandler = { - disposables: this.toDispose, - callback: (index, _event) => { - switch (index) { - case '0': - handleClick(); - break; - } - } - }; - - const hintElement = $('empty-hint-text'); - hintElement.style.display = 'block'; - - const keybindingHint = this.keybindingService.lookupKeybinding(inlineChatId); - const keybindingHintLabel = keybindingHint?.getLabel(); - - if (keybindingHint && keybindingHintLabel) { - const actionPart = localize('emptyHintText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName); - - const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { - if (this.options.clickable) { - const hintPart = $('a', undefined, fragment); - hintPart.style.fontStyle = 'italic'; - hintPart.style.cursor = 'pointer'; - this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); - this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); - return hintPart; - } else { - const hintPart = $('span', undefined, fragment); - hintPart.style.fontStyle = 'italic'; - return hintPart; - } - }); - - hintElement.appendChild(before); - - const label = hintHandler.disposables.add(new KeybindingLabel(hintElement, OS)); - label.set(keybindingHint); - label.element.style.width = 'min-content'; - label.element.style.display = 'inline'; - - if (this.options.clickable) { - label.element.style.cursor = 'pointer'; - this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CONTEXT_MENU, (e) => this._disableHint(e))); - this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); - } - - hintElement.appendChild(after); - - const typeToDismiss = localize('emptyHintTextDismiss', 'Start typing to dismiss.'); - const textHint2 = $('span', undefined, typeToDismiss); - textHint2.style.fontStyle = 'italic'; - hintElement.appendChild(textHint2); - - ariaLabel = actionPart.concat(typeToDismiss); - } else { - const hintMsg = localize({ - key: 'inlineChatHint', - comment: [ - 'Preserve double-square brackets and their order', - ] - }, '[[Ask {0} to do something]] or start typing to dismiss.', providerName); - const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler }); - hintElement.appendChild(rendered); - } - - return { ariaLabel, hintElement }; - } - - private _getHintDefault() { - const hintHandler: IContentActionHandler = { - disposables: this.toDispose, + disposables: this._store, callback: (index, event) => { switch (index) { case '0': - languageOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? askSomething(event.browserEvent) : languageOnClickOrTap(event.browserEvent); break; case '1': - snippetOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? languageOnClickOrTap(event.browserEvent) : snippetOnClickOrTap(event.browserEvent); break; case '2': - chooseEditorOnClickOrTap(event.browserEvent); + hasInlineChatProvider ? snippetOnClickOrTap(event.browserEvent) : chooseEditorOnClickOrTap(event.browserEvent); break; case '3': - this._disableHint(); + this.disableHint(); break; } } }; // the actual command handlers... + const askSomethingCommandId = 'inlineChat.start'; + const askSomething = async (e: UIEvent) => { + e.stopPropagation(); + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: askSomethingCommandId, + from: 'hint' + }); + await this.commandService.executeCommand(askSomethingCommandId, { from: 'hint' }); + }; const languageOnClickOrTap = async (e: UIEvent) => { e.stopPropagation(); // Need to focus editor before so current editor becomes active and the command is properly executed @@ -379,28 +272,33 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { } }; - const hintMsg = localize({ - key: 'message', + const keybindingsLookup = hasInlineChatProvider ? [askSomethingCommandId, ChangeLanguageAction.ID, ApplyFileSnippetAction.Id] : [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; + const keybindingLabels = keybindingsLookup.map(id => this.keybindingService.lookupKeybinding(id)?.getLabel()); + + const hintMsg = (hasInlineChatProvider ? localize({ + key: 'emptyTextEditorHintWithInlineChat', comment: [ 'Preserve double-square brackets and their order', 'language refers to a programming language' ] - }, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.'); + }, '[[Open chat]] ({0}), or [[select a language]] ({1}), or [[fill with template]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '') : localize({ + key: 'emptyTextEditorHintWithoutInlineChat', + comment: [ + 'Preserve double-square brackets and their order', + 'language refers to a programming language' + ] + }, '[[Select a language]] ({0}), or [[fill with template]] ({1}), or [[open a different editor]] ({2}) to get started.\nStart typing to dismiss or [[don\'t show]] this again.', keybindingLabels.at(0) ?? '', keybindingLabels.at(1) ?? '', keybindingLabels.at(2) ?? '')).replaceAll('()', ''); const hintElement = renderFormattedText(hintMsg, { actionHandler: hintHandler, renderCodeSegments: false, }); hintElement.style.fontStyle = 'italic'; - // ugly way to associate keybindings... - const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; - const keybindingLabels = keybindingsLookup.map((id) => this.keybindingService.lookupKeybinding(id)?.getLabel() ?? id); - const ariaLabel = localize('defaultHintAriaLabel', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels); + const ariaLabel = hasInlineChatProvider ? + localize('defaultHintAriaLabelWithInlineChat', 'Execute {0} to ask a question, execute {1} to select a language, or execute {2} to fill with template and get started. Start typing to dismiss.', ...keybindingLabels) : + localize('defaultHintAriaLabelWithoutInlineChat', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels); for (const anchor of hintElement.querySelectorAll('a')) { anchor.style.cursor = 'pointer'; - const id = keybindingsLookup.shift(); - const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel(); - hintHandler.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), anchor, title ?? '')); } return { hintElement, ariaLabel }; @@ -412,12 +310,11 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; - const inlineChatProviders = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)); - const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); + const { hintElement, ariaLabel } = this.getHint(); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); - this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => { + this._register(addDisposableListener(this.domNode, 'click', () => { this.editor.focus(); })); @@ -434,9 +331,10 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { }; } - dispose(): void { + override dispose(): void { + super.dispose(); + this.editor.removeContentWidget(this); - dispose(this.toDispose); } } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index cb2e6e81555..4deb48381eb 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -68,6 +68,7 @@ import { EditSessionsStoreClient } from '../common/editSessionsStorageClient.js' import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceIdentityService } from '../../../services/workspaces/common/workspaceIdentityService.js'; import { hashAsync } from '../../../../base/common/hash.js'; +import { ResourceSet } from '../../../../base/common/map.js'; registerSingleton(IEditSessionsLogService, EditSessionsLogService, InstantiationType.Delayed); registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, InstantiationType.Delayed); @@ -685,6 +686,24 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo // Save all saveable editors before building edit session contents await this.editorService.saveAll(); + // Do a first pass over all repositories to ensure that the edit session identity is created for each. + // This may change the working changes that need to be stored later + const createdEditSessionIdentities = new ResourceSet(); + for (const repository of this.scmService.repositories) { + const changedResources = this.getChangedResources(repository); + if (!changedResources.size) { + continue; + } + for (const uri of changedResources) { + const workspaceFolder = this.contextService.getWorkspaceFolder(uri); + if (!workspaceFolder || createdEditSessionIdentities.has(uri)) { + continue; + } + createdEditSessionIdentities.add(uri); + await this.editSessionIdentityService.onWillCreateEditSessionIdentity(workspaceFolder, cancellationToken); + } + } + for (const repository of this.scmService.repositories) { // Look through all resource groups and compute which files were added/modified/deleted const trackedUris = this.getChangedResources(repository); // A URI might appear in more than one resource group @@ -703,8 +722,6 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo continue; } - await this.editSessionIdentityService.onWillCreateEditSessionIdentity(workspaceFolder, cancellationToken); - name = name ?? workspaceFolder.name; const relativeFilePath = relativePath(workspaceFolder.uri, uri) ?? uri.path; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 4c44d3bb91b..adc061690e0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -11,7 +11,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, SortBy, FilterType, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey } from '../common/extensions.js'; import { InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; @@ -81,6 +81,8 @@ import { IProductService } from '../../../../platform/product/common/productServ import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import product from '../../../../platform/product/common/product.js'; import { ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js'; +import { SearchExtensionsTool, SearchExtensionsToolData } from '../common/searchExtensionsTool.js'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -498,7 +500,8 @@ export const CONTEXT_HAS_REMOTE_SERVER = new RawContextKey('hasRemoteSe export const CONTEXT_HAS_WEB_SERVER = new RawContextKey('hasWebServer', false); const CONTEXT_GALLERY_SORT_CAPABILITIES = new RawContextKey('gallerySortCapabilities', ''); const CONTEXT_GALLERY_FILTER_CAPABILITIES = new RawContextKey('galleryFilterCapabilities', ''); -const CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED = new RawContextKey('galleryAllRepositorySigned', false); +const CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED = new RawContextKey('galleryAllPublicRepositorySigned', false); +const CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED = new RawContextKey('galleryAllPrivateRepositorySigned', false); const CONTEXT_GALLERY_HAS_EXTENSION_LINK = new RawContextKey('galleryHasExtensionLink', false); async function runAction(action: IAction): Promise { @@ -565,7 +568,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi private async registerGalleryCapabilitiesContexts(extensionGalleryManifest: IExtensionGalleryManifest | null): Promise { CONTEXT_GALLERY_SORT_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.sorting?.map(s => s.name)?.join('_')}_UpdateDate_`); CONTEXT_GALLERY_FILTER_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.filtering?.map(s => s.name)?.join('_')}_`); - CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allRepositorySigned); + CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allPublicRepositorySigned); + CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allPrivateRepositorySigned); CONTEXT_GALLERY_HAS_EXTENSION_LINK.bindTo(this.contextKeyService).set(!!(extensionGalleryManifest && getExtensionGalleryManifestResourceUri(extensionGalleryManifest, ExtensionGalleryResourceType.ExtensionDetailsViewUri))); } @@ -1490,7 +1494,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned'), CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned'), + ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED, ContextKeyExpr.not('extensionIsPrivate')), ContextKeyExpr.and(CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED, ContextKeyExpr.has('extensionIsPrivate')))), order: 1 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1963,6 +1968,21 @@ class TrustedPublishersInitializer implements IWorkbenchContribution { } } +class ExtensionToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'extensions.chat.toolsContribution'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + const searchExtensionsTool = instantiationService.createInstance(SearchExtensionsTool); + this._register(toolsService.registerToolData(SearchExtensionsToolData)); + this._register(toolsService.registerToolImplementation(SearchExtensionsToolData.id, searchExtensionsTool)); + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually); @@ -1979,6 +1999,8 @@ if (isWeb) { workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } +registerWorkbenchContribution2(ExtensionToolsContribution.ID, ExtensionToolsContribution, WorkbenchPhase.AfterRestored); + // Running Extensions registerAction2(ShowRuntimeExtensionsAction); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0f0b2bd9341..a9002dc941e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; -import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService, shouldRequireRepositorySignatureFor } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { areSameExtensions, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -499,7 +499,7 @@ export class InstallAction extends ExtensionAction { return; } - if (this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (this.extension.gallery && !this.extension.gallery.isSigned && shouldRequireRepositorySignatureFor(this.extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { const { result } = await this.dialogService.prompt({ type: Severity.Warning, message: localize('not signed', "'{0}' is an extension from an unknown source. Are you sure you want to install?", this.extension.displayName), @@ -1295,6 +1295,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['isExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) === true]); cksOverlay.push(['isPreReleaseExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName, prerelease: true }) === true]); cksOverlay.push(['extensionIsUnsigned', extension.gallery && !extension.gallery.isSigned]); + cksOverlay.push(['extensionIsPrivate', extension.gallery?.private]); const [colorThemes, fileIconThemes, productIconThemes, extensionUsesAuth] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes(), authenticationUsageService.extensionUsesAuth(extension.identifier.id.toLowerCase())]); cksOverlay.push(['extensionHasColorThemes', colorThemes.some(theme => isThemeFromExtension(theme, extension))]); @@ -2583,7 +2584,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned && shouldRequireRepositorySignatureFor(this.extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('not signed tooltip', "This extension is not signed by the Extension Marketplace.")) }, true); return; } @@ -2609,7 +2610,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.outdated && this.extensionsWorkbenchService.isAutoUpdateEnabledFor(this.extension)) { + if (this.extension.outdated) { const message = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension); if (message) { const markdown = new MarkdownString(); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 82f9f53d501..f327b08b0d5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -5,17 +5,17 @@ import * as dom from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; -import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Action } from '../../../../base/common/actions.js'; -import { IExtensionsWorkbenchService, IExtension } from '../common/extensions.js'; +import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; +import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; +import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IListService, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; +import { IListService, IWorkbenchPagedListOptions, WorkbenchAsyncDataTree, WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; -import { IListVirtualDelegate, IListRenderer } from '../../../../base/browser/ui/list/list.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Delegate, Renderer } from './extensionsList.js'; @@ -27,6 +27,125 @@ import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js'; import { getAriaLabelForExtension } from './extensionsViews.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; +import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { ExtensionAction, getContextMenuActions, ManageExtensionAction } from './extensionsActions.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js'; +import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js'; + +export class ExtensionsList extends Disposable { + + readonly list: WorkbenchPagedList; + private readonly contextMenuActionRunner = this._register(new ActionRunner()); + + constructor( + parent: HTMLElement, + viewId: string, + options: Partial>, + extensionsViewState: IExtensionsViewState, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @INotificationService notificationService: INotificationService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error))); + const delegate = new Delegate(); + const renderer = instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = viewDescriptorService.getViewLocationById(viewId); + if (viewLocation === ViewContainerLocation.Sidebar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); + this.list = instantiationService.createInstance(WorkbenchPagedList, `${viewId}-Extensions`, parent, delegate, [renderer], { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(extension: IExtension | null): string { + return getAriaLabelForExtension(extension); + }, + getWidgetAriaLabel(): string { + return localize('extensions', "Extensions"); + } + }, + overrideStyles: getLocationBasedViewColors(viewDescriptorService.getViewLocationById(viewId)).listOverrideStyles, + openOnSingleClick: true, + ...options + }) as WorkbenchPagedList; + this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); + this._register(this.list); + + this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { + this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); + })); + } + + setModel(model: IPagedModel) { + this.list.model = new DelayedPagedModel(model); + } + + layout(height?: number, width?: number): void { + this.list.layout(height, width); + } + + private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { + extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; + this.extensionsWorkbenchService.open(extension, options); + } + + private async onContextMenu(e: IListContextMenuEvent): Promise { + if (e.element) { + const disposables = new DisposableStore(); + const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); + const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element + : e.element; + manageExtensionAction.extension = extension; + let groups: IAction[][] = []; + if (manageExtensionAction.enabled) { + groups = await manageExtensionAction.getActionGroups(); + } else if (extension) { + groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); + groups.forEach(group => group.forEach(extensionAction => { + if (extensionAction instanceof ExtensionAction) { + extensionAction.extension = extension; + } + })); + } + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + actionRunner: this.contextMenuActionRunner, + onHide: () => disposables.dispose() + }); + } + } +} export class ExtensionsGridView extends Disposable { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index dd378644f9c..f6ece8cd747 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { isCancellationError, getErrorMessage, CancellationError } from '../../../../base/common/errors.js'; -import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { PagedModel, IPagedModel, DelayedPagedModel, IPager } from '../../../../base/common/paging.js'; import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -17,7 +16,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Delegate, Renderer } from './extensionsList.js'; import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js'; import { Query } from '../common/extensionQuery.js'; import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js'; @@ -25,7 +23,6 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; -import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from './extensionsActions.js'; import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; @@ -33,23 +30,19 @@ import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from '../../../browse import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { coalesce, distinct, range } from '../../../../base/common/arrays.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { IAction, Action, Separator, ActionRunner } from '../../../../base/common/actions.js'; +import { ActionRunner } from '../../../../base/common/actions.js'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; +import { IViewDescriptorService } from '../../../common/views.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; -import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { isOfflineError } from '../../../../base/parts/request/common/request.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; @@ -59,6 +52,7 @@ import { URI } from '../../../../base/common/uri.js'; import { isString } from '../../../../base/common/types.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { ExtensionsList } from './extensionsViewer.js'; export const NONE_CATEGORY = 'none'; @@ -156,11 +150,9 @@ export class ExtensionsListView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService @@ -196,46 +188,10 @@ export class ExtensionsListView extends ViewPane { const messageSeverityIcon = append(messageContainer, $('')); const messageBox = append(messageContainer, $('.message')); const extensionsList = append(container, $('.extensions-list')); - const delegate = new Delegate(); - this.extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, this.extensionsViewState, { - hoverOptions: { - position: () => { - const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); - if (viewLocation === ViewContainerLocation.Sidebar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; - } - if (viewLocation === ViewContainerLocation.AuxiliaryBar) { - return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; - } - return HoverPosition.RIGHT; - } - } - }); - this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { - multipleSelectionSupport: false, - setRowLineHeight: false, - horizontalScrolling: false, - accessibilityProvider: { - getAriaLabel(extension: IExtension | null): string { - return getAriaLabelForExtension(extension); - }, - getWidgetAriaLabel(): string { - return localize('extensions', "Extensions"); - } - }, - overrideStyles: this.getLocationBasedColors().listOverrideStyles, - openOnSingleClick: true - }) as WorkbenchPagedList; + this.extensionsViewState = this._register(new ExtensionsViewState()); + this.list = this._register(this.instantiationService.createInstance(ExtensionsList, extensionsList, this.id, {}, this.extensionsViewState)).list; ExtensionResultsListFocused.bindTo(this.list.contextKeyService); - this._register(this.list.onContextMenu(e => this.onContextMenu(e), this)); this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this)); - this._register(this.list); - this._register(this.extensionsViewState); - - this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); - })); this.bodyTemplate = { extensionsList, @@ -327,44 +283,6 @@ export class ExtensionsListView extends ViewPane { return Promise.resolve(emptyModel); } - private async onContextMenu(e: IListContextMenuEvent): Promise { - if (e.element) { - const disposables = new DisposableStore(); - const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); - const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element - : e.element; - manageExtensionAction.extension = extension; - let groups: IAction[][] = []; - if (manageExtensionAction.enabled) { - groups = await manageExtensionAction.getActionGroups(); - } else if (extension) { - groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); - groups.forEach(group => group.forEach(extensionAction => { - if (extensionAction instanceof ExtensionAction) { - extensionAction.extension = extension; - } - })); - } - const actions: IAction[] = []; - for (const menuActions of groups) { - for (const menuAction of menuActions) { - actions.push(menuAction); - if (isDisposable(menuAction)) { - disposables.add(menuAction); - } - } - actions.push(new Separator()); - } - actions.pop(); - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions, - actionRunner: this.contextMenuActionRunner, - onHide: () => disposables.dispose() - }); - } - } - private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise { const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g; const ids: string[] = []; @@ -1194,30 +1112,6 @@ export class ExtensionsListView extends ViewPane { } } - private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void { - extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension; - this.extensionsWorkbenchService.open(extension, options).then(undefined, err => this.onError(err)); - } - - private onError(err: any): void { - if (isCancellationError(err)) { - return; - } - - const message = err && err.message || ''; - - if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ]); - - this.notificationService.error(error); - return; - } - - this.notificationService.error(err); - } - override dispose(): void { super.dispose(); if (this.queryRequest) { @@ -1454,11 +1348,9 @@ export class StaticQueryExtensionsView extends ExtensionsListView { @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, - @IPreferencesService preferencesService: IPreferencesService, @IStorageService storageService: IStorageService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService logService: ILogService @@ -1466,7 +1358,7 @@ export class StaticQueryExtensionsView extends ExtensionsListView { super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService, extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService, - preferencesService, storageService, workspaceTrustManagementService, extensionEnablementService, layoutService, extensionFeaturesManagementService, + storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService, uriIdentityService, logService); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 50023799136..00234b7249b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -24,7 +24,8 @@ import { EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, ExtensionManagementError, ExtensionManagementErrorCode, - MaliciousExtensionInfo + MaliciousExtensionInfo, + shouldRequireRepositorySignatureFor } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, findMatchingMaliciousEntry } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -662,7 +663,7 @@ class Extensions extends Disposable { const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist - if (extension.local && extension.local.identifier.uuid !== gallery.identifier.uuid) { + if (extension.local && !extension.local.identifier.uuid) { extension.local = await this.updateMetadata(extension.local, gallery); } if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { @@ -2114,11 +2115,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return; } - if (extension.local?.manifest.main || extension.local?.manifest.browser) { + if (!extension.gallery || !extension.local) { return; } - if (!extension.gallery) { + if (extension.local.identifier.uuid && extension.local.identifier.uuid !== extension.gallery.identifier.uuid) { + return nls.localize('consentRequiredToUpdateRepublishedExtension', "The marketplace metadata of this extension changed, likely due to a re-publish."); + } + + if (!extension.local.manifest.engines.vscode || extension.local.manifest.main || extension.local.manifest.browser) { return; } @@ -2299,7 +2304,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (extension.gallery) { - if (!extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { + if (!extension.gallery.isSigned && shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest())) { return new MarkdownString().appendText(nls.localize('not signed', "This extension is not signed.")); } @@ -2405,7 +2410,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!gallery) { const id = isString(arg) ? arg : (arg).identifier.id; const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest(); - const reportIssueUri = manifest ? getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ReportIssueUri) : undefined; + const reportIssueUri = manifest ? getExtensionGalleryManifestResourceUri(manifest, ExtensionGalleryResourceType.ContactSupportUri) : undefined; const reportIssueMessage = reportIssueUri ? nls.localize('report issue', "If this issue persists, please report it at {0}", reportIssueUri.toString()) : ''; if (installOptions.version) { const message = nls.localize('not found version', "The extension '{0}' cannot be installed because the requested version '{1}' was not found.", id, installOptions.version); @@ -2615,7 +2620,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.withProgress({ location: ProgressLocation.Extensions, - title: nls.localize('uninstallingExtension', 'Uninstalling extension....'), + title: nls.localize('uninstallingExtension', 'Uninstalling extension...'), source: `${extension.identifier.id}` }, () => this.extensionManagementService.uninstallExtensions(extensionsToUninstall).then(() => undefined)); } @@ -2730,7 +2735,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private doInstall(extension: IExtension | undefined, installTask: () => Promise, progressLocation?: ProgressLocation | string): Promise { - const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension....", extension.displayName) : nls.localize('installing extension', 'Installing extension....'); + const title = extension ? nls.localize('installing named extension', "Installing '{0}' extension...", extension.displayName) : nls.localize('installing extension', 'Installing extension...'); return this.withProgress({ location: progressLocation ?? ProgressLocation.Extensions, title diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts new file mode 100644 index 00000000000..3d587cef983 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { localize } from '../../../../nls.js'; +import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { ExtensionState, IExtensionsWorkbenchService } from '../common/extensions.js'; + +export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; + +export const SearchExtensionsToolData: IToolData = { + id: SearchExtensionsToolId, + toolReferenceName: 'extensions', + canBeReferencedInPrompt: true, + icon: ThemeIcon.fromId(Codicon.extensions.id), + supportsToolPicker: true, + displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), + modelDescription: localize('searchExtensionsTool.modelDescription', "This tool helps the model search for VS Code extensions from the Marketplace. The model should specify the category of extensions and keywords to search for. Note that the results may include false positives, so further filtering by reviewing the results is recommended."), + source: { type: 'internal' }, + inputSchema: { + type: 'object', + properties: { + category: { + type: 'string', + description: 'The category of extensions to search for', + enum: EXTENSION_CATEGORIES, + }, + keywords: { + type: 'array', + items: { + type: 'string', + }, + description: 'The keywords to search for', + }, + }, + } +}; + +type InputParams = { + category?: string; + keywords?: string; +}; + +type ExtensionData = { + id: string; + name: string; + description: string; + installed: boolean; + installCount: number; + rating: number; + categories: readonly string[]; + tags: readonly string[]; +}; + +export class SearchExtensionsTool implements IToolImpl { + + constructor( + @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, + ) { } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken): Promise { + const params = invocation.parameters as InputParams; + if (!params.keywords?.length && !params.category) { + return { + content: [{ + kind: 'text', + value: localize('searchExtensionsTool.noInput', 'Please provide a category or keyword to search for.') + }] + }; + } + + const extensionsMap = new Map(); + const queryAndAddExtensions = async (text: string) => { + const extensions = await this.extensionWorkbenchService.queryGallery({ + text, + pageSize: 10, + sortBy: SortBy.InstallCount + }, token); + if (extensions.firstPage.length) { + for (const extension of extensions.firstPage) { + if (extension.deprecationInfo || extension.isMalicious) { + continue; + } + extensionsMap.set(extension.identifier.id.toLowerCase(), { + id: extension.identifier.id, + name: extension.displayName, + description: extension.description, + installed: extension.state === ExtensionState.Installed, + installCount: extension.installCount ?? 0, + rating: extension.rating ?? 0, + categories: extension.categories ?? [], + tags: extension.gallery?.tags ?? [] + }); + } + } + }; + + if (params.keywords?.length) { + for (const keyword of params.keywords ?? []) { + if (keyword === 'featured') { + await queryAndAddExtensions('featured'); + } else { + let text = params.category ? `category:"${params.category}"` : ''; + text = keyword ? `${text} ${keyword}`.trim() : text; + await queryAndAddExtensions(text); + } + } + } else { + await queryAndAddExtensions(`category:"${params.category}"`); + } + + const result = Array.from(extensionsMap.values()); + + return { + content: [{ + kind: 'text', + value: `Here are the list of extensions:\n${JSON.stringify(result)}\n. Use the following format to display extensions to the user because there is a renderer available to parse these extensions in this format and display them with all details. So, do not describe about the extensions to the user.\n\`\`\`vscode-extensions\nextensionId1,extensionId2\n\`\`\`\n.` + }], + toolResultDetails: { + input: JSON.stringify(params), + output: JSON.stringify(result.map(extension => extension.id)) + } + }; + } +} + diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index e1814505d92..13b42f47cca 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -388,6 +388,13 @@ export class CloseAction extends AbstractInline1ChatAction { when: ContextKeyExpr.and( CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() ), + }, { + id: MENU_INLINE_CHAT_SIDE, + group: 'navigation', + when: ContextKeyExpr.and( + CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.None), + CTX_INLINE_CHAT_HAS_AGENT2.negate(), + ) }] }); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index a48cbdd8faf..d365b23ec69 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -16,7 +16,7 @@ import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { IMcpConfiguration, IMcpConfigurationSSE, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IMcpConfiguration, IMcpConfigurationHTTP, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -33,7 +33,7 @@ import { McpServerOptionsCommand } from './mcpCommands.js'; const enum AddConfigurationType { Stdio, - SSE, + HTTP, NpmPackage, PipPackage, @@ -117,7 +117,7 @@ export class McpAddConfigurationCommand { private async getServerType(): Promise { const items: QuickPickInput<{ kind: AddConfigurationType } & IQuickPickItem>[] = [ { kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") }, - { kind: AddConfigurationType.SSE, label: localize('mcp.serverType.http', "HTTP (server-sent events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } + { kind: AddConfigurationType.HTTP, label: localize('mcp.serverType.http', "HTTP (HTTP or Server-Sent Events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } ]; let aiSupported: boolean | undefined; @@ -171,7 +171,7 @@ export class McpAddConfigurationCommand { }; } - private async getSSEConfig(): Promise { + private async getSSEConfig(): Promise { const url = await this._quickInputService.input({ title: localize('mcp.url.title', "Enter Server URL"), placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"), @@ -186,10 +186,7 @@ export class McpAddConfigurationCommand { packageType: 'sse' }); - return { - type: 'sse', - url - }; + return { url }; } private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise { @@ -378,7 +375,7 @@ export class McpAddConfigurationCommand { case AddConfigurationType.Stdio: serverConfig = await this.getStdioConfig(); break; - case AddConfigurationType.SSE: + case AddConfigurationType.HTTP: serverConfig = await this.getSSEConfig(); break; case AddConfigurationType.NpmPackage: @@ -499,7 +496,7 @@ export class McpAddConfigurationCommand { return 'docker'; case AddConfigurationType.Stdio: return 'stdio'; - case AddConfigurationType.SSE: + case AddConfigurationType.HTTP: return 'sse'; default: return undefined; diff --git a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts index 70969d39a39..46943d3a639 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -118,8 +118,8 @@ export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({ id: `${collectionId}.${name}`, label: name, - launch: 'type' in value && value.type === 'sse' ? { - type: McpServerTransportType.SSE, + launch: 'url' in value ? { + type: McpServerTransportType.HTTP, uri: URI.parse(value.url), headers: Object.entries(value.headers || {}), } : { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts index 0abe6cd9824..e425b5d43a7 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -42,7 +42,7 @@ export function claudeConfigToServerDefinition(idPrefix: string, contents: VSBuf id: `${idPrefix}.${name}`, label: name, launch: server.url ? { - type: McpServerTransportType.SSE, + type: McpServerTransportType.HTTP, uri: URI.parse(server.url), headers: [], } : { diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index e29c9d831c4..51c78d8dd7a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -55,6 +55,11 @@ export const mcpSchemaExampleServers = { } }; +const httpSchemaExample = { + url: 'http://localhost:3001/mcp', + headers: {}, +}; + export const mcpStdioServerSchema: IJSONSchema = { type: 'object', additionalProperties: false, @@ -103,27 +108,26 @@ export const mcpServerSchema: IJSONSchema = { additionalProperties: false, properties: { servers: { - examples: [mcpSchemaExampleServers], + examples: [ + mcpSchemaExampleServers, + httpSchemaExample, + ], additionalProperties: { oneOf: [mcpStdioServerSchema, { type: 'object', additionalProperties: false, - required: ['url', 'type'], - examples: [{ - type: 'sse', - url: 'http://localhost:3001', - headers: {}, - }], + required: ['url'], + examples: [httpSchemaExample], properties: { type: { type: 'string', - enum: ['sse'], + enum: ['http', 'sse'], description: localize('app.mcp.json.type', "The type of the server.") }, url: { type: 'string', format: 'uri', - description: localize('app.mcp.json.url', "The URL of the server-sent-event (SSE) server.") + description: localize('app.mcp.json.url', "The URL of the Streamable HTTP or SSE endpoint.") }, headers: { type: 'object', diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 42990525bef..65ba4094e3a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -326,7 +326,12 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } public async resolveConnection({ collectionRef, definitionRef, forceTrust, logger }: IMcpResolveConnectionOptions): Promise { - const collection = this._collections.get().find(c => c.id === collectionRef.id); + let collection = this._collections.get().find(c => c.id === collectionRef.id); + if (collection?.lazy) { + await collection.lazy.load(); + collection = this._collections.get().find(c => c.id === collectionRef.id); + } + const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id); if (!collection || !definition) { throw new Error(`Collection or definition not found for ${collectionRef.id} and ${definitionRef.id}`); @@ -356,7 +361,14 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } } - let launch: McpServerLaunch | undefined; + let launch: McpServerLaunch | undefined = definition.launch; + if (collection.resolveServerLanch) { + launch = await collection.resolveServerLanch(definition); + if (!launch) { + return undefined; // interaction cancelled by user + } + } + try { launch = await this._replaceVariablesInLaunch(definition, definition.launch); } catch (e) { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index e565f38cab3..6545737fb4a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -250,7 +250,7 @@ export class McpServer extends Disposable implements IMcpServer { const serverTools = this.toolsFromServer.read(reader); const definitions = serverTools ?? this.toolsFromCache ?? []; const prefix = toolPrefix.read(reader); - return definitions.map(def => new McpTool(this, prefix, def)); + return definitions.map(def => new McpTool(this, prefix, def)).sort((a, b) => a.compare(b)); }); } @@ -483,6 +483,10 @@ export class McpTool implements IMcpTool { const name = this._definition.serverToolName ?? this._definition.name; return this._server.callOn(h => h.callTool({ name, arguments: params }), token); } + + compare(other: IMcpTool): number { + return this._definition.name.localeCompare(other.definition.name); + } } function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) { diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index 38666d83ca1..98a9bddf4e2 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -98,9 +98,9 @@ export class McpService extends Disposable implements IMcpService { const collection = this._mcpRegistry.collections.get().find(c => c.id === server.collection.id); const toolData: IToolData = { id: tool.id, - source: { type: 'mcp', collectionId: server.collection.id, definitionId: server.definition.id }, + source: { type: 'mcp', label: server.definition.label, collectionId: server.collection.id, definitionId: server.definition.id }, icon: Codicon.tools, - displayName: tool.definition.name, + displayName: tool.definition.annotations?.title || tool.definition.name, toolReferenceName: tool.definition.name, modelDescription: tool.definition.description ?? '', userDescription: tool.definition.description ?? '', @@ -228,14 +228,17 @@ class McpToolImplementation implements IToolImpl { this._productService.nameShort ); + const needsConfirmation = !tool.definition.annotations?.readOnlyHint; + const title = tool.definition.annotations?.title || ('`' + tool.definition.name + '`'); + return { - confirmationMessages: { - title: localize('msg.title', "Run `{0}`", tool.definition.name, server.definition.label), + confirmationMessages: needsConfirmation ? { + title: localize('msg.title', "Run {0}", title), message: new MarkdownString(localize('msg.msg', "{0}\n\n {1}", tool.definition.description, mcpToolWarning), { supportThemeIcons: true }), allowAutoConfirm: true, - }, - invocationMessage: new MarkdownString(localize('msg.run', "Running `{0}`", tool.definition.name, server.definition.label)), - pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran `{0}` ", tool.definition.name, server.definition.label)), + } : undefined, + invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)), + pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran {0} ", title)), toolSpecificData: { kind: 'input', rawInput: parameters diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 6c4a2540878..a590925c9a8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -44,6 +44,9 @@ export interface McpCollectionDefinition { /** Scope where associated collection info should be stored. */ readonly scope: StorageScope; + /** Resolves a server definition. If present, always called before a server starts. */ + resolveServerLanch?(definition: McpServerDefinition): Promise; + /** For lazy-loaded collections only: */ readonly lazy?: { /** True if `serverDefinitions` were loaded from the cache */ @@ -78,6 +81,8 @@ export namespace McpCollectionDefinition { readonly label: string; readonly isTrustedByDefault: boolean; readonly scope: StorageScope; + readonly canResolveLaunch: boolean; + readonly extensionId: string; } export function equals(a: McpCollectionDefinition, b: McpCollectionDefinition): boolean { @@ -254,7 +259,7 @@ export const enum McpServerTransportType { /** A command-line MCP server communicating over standard in/out */ Stdio = 1 << 0, /** An MCP server that uses Server-Sent Events */ - SSE = 1 << 1, + HTTP = 1 << 1, } /** @@ -271,22 +276,23 @@ export interface McpServerTransportStdio { } /** - * MCP server launched on the command line which communicated over server-sent-events. + * MCP server launched on the command line which communicated over SSE or Streamable HTTP. * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse + * https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http */ -export interface McpServerTransportSSE { - readonly type: McpServerTransportType.SSE; +export interface McpServerTransportHTTP { + readonly type: McpServerTransportType.HTTP; readonly uri: URI; readonly headers: [string, string][]; } export type McpServerLaunch = | McpServerTransportStdio - | McpServerTransportSSE; + | McpServerTransportHTTP; export namespace McpServerLaunch { export type Serialized = - | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } + | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][] } | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { @@ -295,7 +301,7 @@ export namespace McpServerLaunch { export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { switch (launch.type) { - case McpServerTransportType.SSE: + case McpServerTransportType.HTTP: return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; case McpServerTransportType.Stdio: return { diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts index 582e69eaf93..b5f8eeeff62 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable @stylistic/ts/member-delimiter-style */ /* eslint-disable local/code-no-unexternalized-strings */ /** @@ -13,24 +12,38 @@ * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ */ export namespace MCP { - /* JSON-RPC types */ + /** + * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. + */ export type JSONRPCMessage = | JSONRPCRequest | JSONRPCNotification + | JSONRPCBatchRequest | JSONRPCResponse - | JSONRPCError; + | JSONRPCError + | JSONRPCBatchResponse; - export const LATEST_PROTOCOL_VERSION = "2024-11-05"; + /** + * A JSON-RPC batch request, as described in https://www.jsonrpc.org/specification#batch. + */ + export type JSONRPCBatchRequest = (JSONRPCRequest | JSONRPCNotification)[]; + + /** + * A JSON-RPC batch response, as described in https://www.jsonrpc.org/specification#batch. + */ + export type JSONRPCBatchResponse = (JSONRPCResponse | JSONRPCError)[]; + + export const LATEST_PROTOCOL_VERSION = "2025-03-26"; export const JSONRPC_VERSION = "2.0"; /** - * A progress token, used to associate progress notifications with the original request. - */ + * A progress token, used to associate progress notifications with the original request. + */ export type ProgressToken = string | number; /** - * An opaque token used to represent a cursor for pagination. - */ + * An opaque token used to represent a cursor for pagination. + */ export type Cursor = string; export interface Request { @@ -66,28 +79,28 @@ export namespace MCP { } /** - * A uniquely identifying ID for a request in JSON-RPC. - */ + * A uniquely identifying ID for a request in JSON-RPC. + */ export type RequestId = string | number; /** - * A request that expects a response. - */ + * A request that expects a response. + */ export interface JSONRPCRequest extends Request { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; } /** - * A notification which does not expect a response. - */ + * A notification which does not expect a response. + */ export interface JSONRPCNotification extends Notification { jsonrpc: typeof JSONRPC_VERSION; } /** - * A successful (non-error) response to a request. - */ + * A successful (non-error) response to a request. + */ export interface JSONRPCResponse { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; @@ -102,8 +115,8 @@ export namespace MCP { export const INTERNAL_ERROR = -32603; /** - * A response to a request that indicates an error occurred. - */ + * A response to a request that indicates an error occurred. + */ export interface JSONRPCError { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; @@ -125,20 +138,20 @@ export namespace MCP { /* Empty result */ /** - * A response that indicates success but carries no data. - */ + * A response that indicates success but carries no data. + */ export type EmptyResult = Result; /* Cancellation */ /** - * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its `initialize` request. - */ + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + */ export interface CancelledNotification extends Notification { method: "notifications/cancelled"; params: { @@ -158,8 +171,8 @@ export namespace MCP { /* Initialization */ /** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - */ + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ export interface InitializeRequest extends Request { method: "initialize"; params: { @@ -173,8 +186,8 @@ export namespace MCP { } /** - * After receiving an initialize request from the client, the server sends this response. - */ + * After receiving an initialize request from the client, the server sends this response. + */ export interface InitializeResult extends Result { /** * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. @@ -182,6 +195,7 @@ export namespace MCP { protocolVersion: string; capabilities: ServerCapabilities; serverInfo: Implementation; + /** * Instructions describing how to use the server and its features. * @@ -191,15 +205,15 @@ export namespace MCP { } /** - * This notification is sent from the client to the server after initialization has finished. - */ + * This notification is sent from the client to the server after initialization has finished. + */ export interface InitializedNotification extends Notification { method: "notifications/initialized"; } /** - * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - */ + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + */ export interface ClientCapabilities { /** * Experimental, non-standard capabilities that the client supports. @@ -221,8 +235,8 @@ export namespace MCP { } /** - * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - */ + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + */ export interface ServerCapabilities { /** * Experimental, non-standard capabilities that the server supports. @@ -232,6 +246,10 @@ export namespace MCP { * Present if the server supports sending log messages to the client. */ logging?: object; + /** + * Present if the server supports argument autocompletion suggestions. + */ + completions?: object; /** * Present if the server offers any prompt templates. */ @@ -266,8 +284,8 @@ export namespace MCP { } /** - * Describes the name and version of an MCP implementation. - */ + * Describes the name and version of an MCP implementation. + */ export interface Implementation { name: string; version: string; @@ -275,16 +293,16 @@ export namespace MCP { /* Ping */ /** - * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. - */ + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + */ export interface PingRequest extends Request { method: "ping"; } /* Progress notifications */ /** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - */ + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + */ export interface ProgressNotification extends Notification { method: "notifications/progress"; params: { @@ -304,6 +322,10 @@ export namespace MCP { * @TJS-type number */ total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; }; } @@ -328,36 +350,36 @@ export namespace MCP { /* Resources */ /** - * Sent from the client to request a list of resources the server has. - */ + * Sent from the client to request a list of resources the server has. + */ export interface ListResourcesRequest extends PaginatedRequest { method: "resources/list"; } /** - * The server's response to a resources/list request from the client. - */ + * The server's response to a resources/list request from the client. + */ export interface ListResourcesResult extends PaginatedResult { resources: Resource[]; } /** - * Sent from the client to request a list of resource templates the server has. - */ + * Sent from the client to request a list of resource templates the server has. + */ export interface ListResourceTemplatesRequest extends PaginatedRequest { method: "resources/templates/list"; } /** - * The server's response to a resources/templates/list request from the client. - */ + * The server's response to a resources/templates/list request from the client. + */ export interface ListResourceTemplatesResult extends PaginatedResult { resourceTemplates: ResourceTemplate[]; } /** - * Sent from the client to the server, to read a specific resource URI. - */ + * Sent from the client to the server, to read a specific resource URI. + */ export interface ReadResourceRequest extends Request { method: "resources/read"; params: { @@ -371,22 +393,22 @@ export namespace MCP { } /** - * The server's response to a resources/read request from the client. - */ + * The server's response to a resources/read request from the client. + */ export interface ReadResourceResult extends Result { contents: (TextResourceContents | BlobResourceContents)[]; } /** - * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + */ export interface ResourceListChangedNotification extends Notification { method: "notifications/resources/list_changed"; } /** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. - */ + * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + */ export interface SubscribeRequest extends Request { method: "resources/subscribe"; params: { @@ -400,8 +422,8 @@ export namespace MCP { } /** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. - */ + * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + */ export interface UnsubscribeRequest extends Request { method: "resources/unsubscribe"; params: { @@ -415,8 +437,8 @@ export namespace MCP { } /** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. - */ + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + */ export interface ResourceUpdatedNotification extends Notification { method: "notifications/resources/updated"; params: { @@ -430,9 +452,9 @@ export namespace MCP { } /** - * A known resource that the server is capable of reading. - */ - export interface Resource extends Annotated { + * A known resource that the server is capable of reading. + */ + export interface Resource { /** * The URI of this resource. * @@ -459,6 +481,11 @@ export namespace MCP { */ mimeType?: string; + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + /** * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. * @@ -468,9 +495,9 @@ export namespace MCP { } /** - * A template description for resources available on the server. - */ - export interface ResourceTemplate extends Annotated { + * A template description for resources available on the server. + */ + export interface ResourceTemplate { /** * A URI template (according to RFC 6570) that can be used to construct resource URIs. * @@ -496,11 +523,16 @@ export namespace MCP { * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. */ mimeType?: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * The contents of a specific resource or sub-resource. - */ + * The contents of a specific resource or sub-resource. + */ export interface ResourceContents { /** * The URI of this resource. @@ -532,22 +564,22 @@ export namespace MCP { /* Prompts */ /** - * Sent from the client to request a list of prompts and prompt templates the server has. - */ + * Sent from the client to request a list of prompts and prompt templates the server has. + */ export interface ListPromptsRequest extends PaginatedRequest { method: "prompts/list"; } /** - * The server's response to a prompts/list request from the client. - */ + * The server's response to a prompts/list request from the client. + */ export interface ListPromptsResult extends PaginatedResult { prompts: Prompt[]; } /** - * Used by the client to get a prompt provided by the server. - */ + * Used by the client to get a prompt provided by the server. + */ export interface GetPromptRequest extends Request { method: "prompts/get"; params: { @@ -563,8 +595,8 @@ export namespace MCP { } /** - * The server's response to a prompts/get request from the client. - */ + * The server's response to a prompts/get request from the client. + */ export interface GetPromptResult extends Result { /** * An optional description for the prompt. @@ -574,8 +606,8 @@ export namespace MCP { } /** - * A prompt or prompt template that the server offers. - */ + * A prompt or prompt template that the server offers. + */ export interface Prompt { /** * The name of the prompt or prompt template. @@ -592,8 +624,8 @@ export namespace MCP { } /** - * Describes an argument that a prompt can accept. - */ + * Describes an argument that a prompt can accept. + */ export interface PromptArgument { /** * The name of the argument. @@ -610,68 +642,73 @@ export namespace MCP { } /** - * The sender or recipient of messages and data in a conversation. - */ + * The sender or recipient of messages and data in a conversation. + */ export type Role = "user" | "assistant"; /** - * Describes a message returned as part of a prompt. - * - * This is similar to `SamplingMessage`, but also supports the embedding of - * resources from the MCP server. - */ + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + */ export interface PromptMessage { role: Role; - content: TextContent | ImageContent | EmbeddedResource; + content: TextContent | ImageContent | AudioContent | EmbeddedResource; } /** - * The contents of a resource, embedded into a prompt or tool call result. - * - * It is up to the client how best to render embedded resources for the benefit - * of the LLM and/or the user. - */ - export interface EmbeddedResource extends Annotated { + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + */ + export interface EmbeddedResource { type: "resource"; resource: TextResourceContents | BlobResourceContents; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + */ export interface PromptListChangedNotification extends Notification { method: "notifications/prompts/list_changed"; } /* Tools */ /** - * Sent from the client to request a list of tools the server has. - */ + * Sent from the client to request a list of tools the server has. + */ export interface ListToolsRequest extends PaginatedRequest { method: "tools/list"; } /** - * The server's response to a tools/list request from the client. - */ + * The server's response to a tools/list request from the client. + */ export interface ListToolsResult extends PaginatedResult { tools: Tool[]; } /** - * The server's response to a tool call. - * - * Any errors that originate from the tool SHOULD be reported inside the result - * object, with `isError` set to true, _not_ as an MCP protocol-level error - * response. Otherwise, the LLM would not be able to see that an error occurred - * and self-correct. - * - * However, any errors in _finding_ the tool, an error indicating that the - * server does not support tool calls, or any other exceptional conditions, - * should be reported as an MCP error response. - */ + * The server's response to a tool call. + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ export interface CallToolResult extends Result { - content: (TextContent | ImageContent | EmbeddedResource)[]; + content: (TextContent | ImageContent | AudioContent | EmbeddedResource)[]; /** * Whether the tool call ended in an error. @@ -682,8 +719,8 @@ export namespace MCP { } /** - * Used by the client to invoke a tool provided by the server. - */ + * Used by the client to invoke a tool provided by the server. + */ export interface CallToolRequest extends Request { method: "tools/call"; params: { @@ -693,24 +730,82 @@ export namespace MCP { } /** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. - */ + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + */ export interface ToolListChangedNotification extends Notification { method: "notifications/tools/list_changed"; } /** - * Definition for a tool the client can call. - */ + * Additional properties describing a Tool to clients. + * + * NOTE: all properties in ToolAnnotations are **hints**. + * They are not guaranteed to provide a faithful description of + * tool behavior (including descriptive properties like `title`). + * + * Clients should never make tool use decisions based on ToolAnnotations + * received from untrusted servers. + */ + export interface ToolAnnotations { + /** + * A human-readable title for the tool. + */ + title?: string; + + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint?: boolean; + + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint?: boolean; + + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on the its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint?: boolean; + + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint?: boolean; + } + + /** + * Definition for a tool the client can call. + */ export interface Tool { /** * The name of the tool. */ name: string; + /** * A human-readable description of the tool. + * + * This can be used by clients to improve the LLM's understanding of available tools. It can be thought of like a "hint" to the model. */ description?: string; + /** * A JSON Schema object defining the expected parameters for the tool. */ @@ -719,12 +814,17 @@ export namespace MCP { properties?: { [key: string]: object }; required?: string[]; }; + + /** + * Optional additional tool information. + */ + annotations?: ToolAnnotations; } /* Logging */ /** - * A request from the client to the server, to enable or adjust logging. - */ + * A request from the client to the server, to enable or adjust logging. + */ export interface SetLevelRequest extends Request { method: "logging/setLevel"; params: { @@ -736,8 +836,8 @@ export namespace MCP { } /** - * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. - */ + * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + */ export interface LoggingMessageNotification extends Notification { method: "notifications/message"; params: { @@ -757,11 +857,11 @@ export namespace MCP { } /** - * The severity of a log message. - * - * These map to syslog message severities, as specified in RFC-5424: - * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - */ + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + */ export type LoggingLevel = | "debug" | "info" @@ -774,8 +874,8 @@ export namespace MCP { /* Sampling */ /** - * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - */ + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + */ export interface CreateMessageRequest extends Request { method: "sampling/createMessage"; params: { @@ -809,8 +909,8 @@ export namespace MCP { } /** - * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. - */ + * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. + */ export interface CreateMessageResult extends Result, SamplingMessage { /** * The name of the model that generated the message. @@ -823,81 +923,116 @@ export namespace MCP { } /** - * Describes a message issued to or received from an LLM API. - */ + * Describes a message issued to or received from an LLM API. + */ export interface SamplingMessage { role: Role; - content: TextContent | ImageContent; + content: TextContent | ImageContent | AudioContent; } /** - * Base for objects that include optional annotations for the client. The client can use annotations to inform how objects are used or displayed - */ - export interface Annotated { - annotations?: { - /** - * Describes who the intended customer of this object or data is. - * - * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). - */ - audience?: Role[]; + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ + export interface Annotations { + /** + * Describes who the intended customer of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; - /** - * Describes how important this data is for operating the server. - * - * A value of 1 means "most important," and indicates that the data is - * effectively required, while 0 means "least important," and indicates that - * the data is entirely optional. - * - * @TJS-type number - * @minimum 0 - * @maximum 1 - */ - priority?: number; - } + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; } /** - * Text provided to or from an LLM. - */ - export interface TextContent extends Annotated { + * Text provided to or from an LLM. + */ + export interface TextContent { type: "text"; + /** * The text content of the message. */ text: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * An image provided to or from an LLM. - */ - export interface ImageContent extends Annotated { + * An image provided to or from an LLM. + */ + export interface ImageContent { type: "image"; + /** * The base64-encoded image data. * * @format byte */ data: string; + /** * The MIME type of the image. Different providers may support different image types. */ mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; } /** - * The server's preferences for model selection, requested of the client during sampling. - * - * Because LLMs can vary along multiple dimensions, choosing the "best" model is - * rarely straightforward. Different models excel in different areas-some are - * faster but less capable, others are more capable but more expensive, and so - * on. This interface allows servers to express their priorities across multiple - * dimensions to help clients make an appropriate selection for their use case. - * - * These preferences are always advisory. The client MAY ignore them. It is also - * up to the client to decide how to interpret these preferences and how to - * balance them against other considerations. - */ + * Audio provided to or from an LLM. + */ + export interface AudioContent { + type: "audio"; + + /** + * The base64-encoded audio data. + * + * @format byte + */ + data: string; + + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + } + + /** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas-some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + */ export interface ModelPreferences { /** * Optional hints to use for model selection. @@ -945,11 +1080,11 @@ export namespace MCP { } /** - * Hints to use for model selection. - * - * Keys not declared here are currently left unspecified by the spec and are up - * to the client to interpret. - */ + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + */ export interface ModelHint { /** * A hint for a model name. @@ -967,8 +1102,8 @@ export namespace MCP { /* Autocomplete */ /** - * A request from the client to the server, to ask for completion options. - */ + * A request from the client to the server, to ask for completion options. + */ export interface CompleteRequest extends Request { method: "completion/complete"; params: { @@ -990,8 +1125,8 @@ export namespace MCP { } /** - * The server's response to a completion/complete request - */ + * The server's response to a completion/complete request + */ export interface CompleteResult extends Result { completion: { /** @@ -1010,8 +1145,8 @@ export namespace MCP { } /** - * A reference to a resource or resource template definition. - */ + * A reference to a resource or resource template definition. + */ export interface ResourceReference { type: "ref/resource"; /** @@ -1023,8 +1158,8 @@ export namespace MCP { } /** - * Identifies a prompt. - */ + * Identifies a prompt. + */ export interface PromptReference { type: "ref/prompt"; /** @@ -1035,30 +1170,30 @@ export namespace MCP { /* Roots */ /** - * Sent from the server to request a list of root URIs from the client. Roots allow - * servers to ask for specific directories or files to operate on. A common example - * for roots is providing a set of repositories or directories a server should operate - * on. - * - * This request is typically used when the server needs to understand the file system - * structure or access specific locations that the client has permission to read from. - */ + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + */ export interface ListRootsRequest extends Request { method: "roots/list"; } /** - * The client's response to a roots/list request from the server. - * This result contains an array of Root objects, each representing a root directory - * or file that the server can operate on. - */ + * The client's response to a roots/list request from the server. + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. + */ export interface ListRootsResult extends Result { roots: Root[]; } /** - * Represents a root directory or file that the server can operate on. - */ + * Represents a root directory or file that the server can operate on. + */ export interface Root { /** * The URI identifying the root. This *must* start with file:// for now. @@ -1077,10 +1212,10 @@ export namespace MCP { } /** - * A notification from the client to the server, informing it that the list of roots has changed. - * This notification should be sent whenever the client adds, removes, or modifies any root. - * The server should then request an updated list of roots using the ListRootsRequest. - */ + * A notification from the client to the server, informing it that the list of roots has changed. + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + */ export interface RootsListChangedNotification extends Notification { method: "notifications/roots/list_changed"; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index 584fc890a92..dfd645ce241 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -15,6 +15,7 @@ import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { Event } from '../../../../../../base/common/event.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { autorun } from '../../../../../../base/common/observable.js'; export class CellDiagnostics extends Disposable implements INotebookEditorContribution { @@ -121,6 +122,11 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri disposables.push(toDisposable(() => this.markerService.changeOne(CellDiagnostics.ID, cell.uri, []))); cell.executionErrorDiagnostic.set(metadata.error, undefined); disposables.push(toDisposable(() => cell.executionErrorDiagnostic.set(undefined, undefined))); + disposables.push(autorun((r) => { + if (!cell.executionErrorDiagnostic.read(r)) { + this.clear(cellHandle); + } + })); disposables.push(cell.model.onDidChangeOutputs(() => { if (cell.model.outputs.length === 0) { this.clear(cellHandle); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index 7df058a6901..0870ac79d1b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -6,64 +6,41 @@ import { Schemas } from '../../../../../../base/common/network.js'; import { ICodeEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../../../editor/browser/editorExtensions.js'; -import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; -import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; -import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; -import { IProductService } from '../../../../../../platform/product/common/productService.js'; -import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { EmptyTextEditorHintContribution, IEmptyTextEditorHintOptions } from '../../../../codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.js'; +import { EmptyTextEditorHintContribution } from '../../../../codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.js'; import { IInlineChatSessionService } from '../../../../inlineChat/browser/inlineChatSessionService.js'; import { getNotebookEditorFromEditorPane } from '../../notebookBrowser.js'; -import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribution { public static readonly CONTRIB_ID = 'notebook.editor.contrib.emptyCellEditorHint'; constructor( editor: ICodeEditor, @IEditorService private readonly _editorService: IEditorService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService, - @ICommandService commandService: ICommandService, @IConfigurationService configurationService: IConfigurationService, - @IHoverService hoverService: IHoverService, - @IKeybindingService keybindingService: IKeybindingService, @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, @IChatAgentService chatAgentService: IChatAgentService, - @ITelemetryService telemetryService: ITelemetryService, - @IProductService productService: IProductService, - @IContextMenuService contextMenuService: IContextMenuService + @IInstantiationService instantiationService: IInstantiationService ) { super( editor, - editorGroupsService, - commandService, configurationService, - hoverService, - keybindingService, inlineChatSessionService, chatAgentService, - telemetryService, - productService, - contextMenuService + instantiationService ); const activeEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); - if (!activeEditor) { return; } - this.toDispose.push(activeEditor.onDidChangeActiveCell(() => this.update())); + this._register(activeEditor.onDidChangeActiveCell(() => this.update())); } - protected override _getOptions(): IEmptyTextEditorHintOptions { - return { clickable: false }; - } - - protected override _shouldRenderHint(): boolean { + protected override shouldRenderHint(): boolean { const model = this.editor.getModel(); if (!model) { return false; @@ -79,7 +56,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu return false; } - const shouldRenderHint = super._shouldRenderHint(); + const shouldRenderHint = super.shouldRenderHint(); if (!shouldRenderHint) { return false; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index 6f62cf7ca2f..6993def3c8a 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -249,7 +249,8 @@ registerAction2(class CopyCellOutputAction extends Action2 { menu: { id: MenuId.NotebookOutputToolbar, when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, ContextKeyExpr.in(NOTEBOOK_CELL_OUTPUT_MIMETYPE.key, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT.key)), - order: 10 + order: 10, + group: 'notebook_chat_actions' }, category: NOTEBOOK_ACTIONS_CATEGORY, icon: icons.copyIcon, diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 071190afa9a..20cd1c5aee0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -329,9 +329,7 @@ class CellOutputElement extends Disposable { const isFirstCellOutput = NOTEBOOK_CELL_IS_FIRST_OUTPUT.bindTo(menuContextKeyService); const cellOutputMimetype = NOTEBOOK_CELL_OUTPUT_MIMETYPE.bindTo(menuContextKeyService); isFirstCellOutput.set(index === 0); - if (mimeTypes[index]) { - cellOutputMimetype.set(currentMimeType.mimeType); - } + cellOutputMimetype.set(currentMimeType.mimeType); this.toolbarDisposables.add(autorun((r) => { hasHiddenOutputs.set(this.cellOutputContainer.hasHiddenOutputs.read(r)); })); const menu = this.toolbarDisposables.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, menuContextKeyService)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 39a7a7b4a48..26f94306030 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -2895,6 +2895,7 @@ async function webviewPreloads(ctx: PreloadContext) { private hasResizeObserver = false; private renderTaskAbort?: AbortController; + private isImageOutput = false; constructor( private readonly outputId: string, @@ -2909,9 +2910,6 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.left = left + 'px'; this.element.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}`; - // Make output draggable - this.element.draggable = true; - this.element.addEventListener('mouseenter', () => { postNotebookMessage('mouseenter', { id: outputId }); }); @@ -2931,6 +2929,24 @@ async function webviewPreloads(ctx: PreloadContext) { e.dataTransfer.setData('notebook-cell-output', JSON.stringify(outputData)); }); + + // Add alt key handlers + window.addEventListener('keydown', (e) => { + if (e.altKey) { + this.element.draggable = true; + } + }); + + window.addEventListener('keyup', (e) => { + if (!e.altKey) { + this.element.draggable = this.isImageOutput; + } + }); + + // Handle window blur to reset draggable state + window.addEventListener('blur', () => { + this.element.draggable = this.isImageOutput; + }); } public dispose() { @@ -2950,6 +2966,11 @@ async function webviewPreloads(ctx: PreloadContext) { const errors = preloadErrors.filter((e): e is Error => e instanceof Error); showRenderError(`Error loading preloads`, this.element, errors); } else { + + const imageMimeTypes = ['image/png', 'image/jpeg', 'image/svg']; + this.isImageOutput = imageMimeTypes.includes(content.output.mime); + this.element.draggable = this.isImageOutput; + const item = createOutputItem(this.outputId, content.output.mime, content.metadata, content.output.valueBytes, content.allOutputs, content.output.appended); const controller = new AbortController(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index 7b8f08de267..1077d603543 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -158,12 +158,12 @@ suite('notebookCellDiagnostics', () => { testExecutionService.fireExecutionChanged(editor.textModel.uri, cell2.handle); await new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); - cell.model.internalMetadata.error = undefined; + const clearMarkers = new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); // on NotebookCellExecution value will make it look like its currently running testExecutionService.fireExecutionChanged(editor.textModel.uri, cell.handle, {} as INotebookCellExecution); - await new Promise(resolve => Event.once(markerService.onMarkersUpdated)(resolve)); + await clearMarkers; assert.strictEqual(cell?.executionErrorDiagnostic.get(), undefined); assert.strictEqual(cell2?.executionErrorDiagnostic.get()?.message, 'another bad thing happened', 'cell that was not executed should still have an error'); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index aa1fb1c0536..287134403a9 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -593,11 +593,15 @@ padding-bottom: 26px; } -.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-value-description { +.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-description { display: flex; cursor: pointer; } +.settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-item-description.disabled { + cursor: initial; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-value-checkbox { height: 18px; width: 18px; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 2f49546c8e4..caa55406a72 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1989,7 +1989,7 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label')); const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer)); - const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); + const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement)); const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); @@ -2008,20 +2008,6 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender template.onChange!(checkbox.checked); })); - // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety - // Also have to ignore embedded links - too buried to stop propagation - toDispose.add(DOM.addDisposableListener(descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { - const targetElement = e.target; - - // Toggle target checkbox - if (targetElement.tagName.toLowerCase() !== 'a') { - template.checkbox.checked = !template.checkbox.checked; - template.onChange!(checkbox.checked); - } - DOM.EventHelper.stop(e); - })); - - checkbox.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); const toolbar = this.renderSettingToolbar(toolbarContainer); @@ -2059,6 +2045,26 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void { template.onChange = undefined; template.checkbox.checked = dataElement.value; + if (dataElement.hasPolicyValue) { + template.checkbox.disable(); + template.descriptionElement.classList.add('disabled'); + } else { + template.checkbox.enable(); + template.descriptionElement.classList.remove('disabled'); + + // Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety + // Also have to ignore embedded links - too buried to stop propagation + template.elementDisposables.add(DOM.addDisposableListener(template.descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => { + const targetElement = e.target; + + // Toggle target checkbox + if (targetElement.tagName.toLowerCase() !== 'a') { + template.checkbox.checked = !template.checkbox.checked; + template.onChange!(template.checkbox.checked); + } + DOM.EventHelper.stop(e); + })); + } template.checkbox.setTitle(dataElement.setting.key); template.onChange = onChange; } diff --git a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css index 5a9e4a2536e..889f1b63fdd 100644 --- a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css @@ -25,55 +25,89 @@ display: none; } -.monaco-editor .dirty-diff-added { - border-left-color: var(--vscode-editorGutter-addedBackground); +.monaco-editor .dirty-diff-added:not(.pattern) { border-left-style: solid; } -.monaco-editor .dirty-diff-added:before { +.monaco-editor .dirty-diff-added.primary { + border-left-color: var(--vscode-editorGutter-addedBackground); +} + +.monaco-editor .dirty-diff-added.primary:before { background: var(--vscode-editorGutter-addedBackground); } -.monaco-editor .dirty-diff-added-pattern { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-added.secondary { + border-left-color: var(--vscode-editorGutter-addedSecondaryBackground); +} + +.monaco-editor .dirty-diff-added.secondary:before { + background: var(--vscode-editorGutter-addedSecondaryBackground); +} + +.monaco-editor .dirty-diff-added.pattern { background-repeat: repeat-y; } -.monaco-editor .dirty-diff-added-pattern:before { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-added.pattern:before { transform: translateX(3px); } -.monaco-editor .dirty-diff-modified { - border-left-color: var(--vscode-editorGutter-modifiedBackground); +.monaco-editor .dirty-diff-added.pattern.primary, +.monaco-editor .dirty-diff-added.pattern.primary:before { + background-image: linear-gradient(-45deg, var(--vscode-editorGutter-addedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedBackground) 50%, var(--vscode-editorGutter-addedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-editor .dirty-diff-added.pattern.secondary, +.monaco-editor .dirty-diff-added.pattern.secondary:before { + background-image: linear-gradient(45deg, var(--vscode-editorGutter-addedSecondaryBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-addedSecondaryBackground) 50%, var(--vscode-editorGutter-addedSecondaryBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-editor .dirty-diff-modified:not(.pattern) { border-left-style: solid; } -.monaco-editor .dirty-diff-modified:before { +.monaco-editor .dirty-diff-modified.primary { + border-left-color: var(--vscode-editorGutter-modifiedBackground); +} + +.monaco-editor .dirty-diff-modified.primary:before { background: var(--vscode-editorGutter-modifiedBackground); } -.monaco-editor .dirty-diff-modified-pattern { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-modifiedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedBackground) 50%, var(--vscode-editorGutter-modifiedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-modified.secondary { + border-left-color: var(--vscode-editorGutter-modifiedSecondaryBackground); +} + +.monaco-editor .dirty-diff-modified.secondary:before { + background: var(--vscode-editorGutter-modifiedSecondaryBackground); +} + +.monaco-editor .dirty-diff-modified.pattern { background-repeat: repeat-y; } -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added-pattern, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified, -.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified-pattern { - transition: opacity 0.5s; -} - -.monaco-editor .dirty-diff-modified-pattern:before { - background-image: linear-gradient(-45deg, var(--vscode-editorGutter-modifiedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedBackground) 50%, var(--vscode-editorGutter-modifiedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +.monaco-editor .dirty-diff-modified.pattern:before { transform: translateX(3px); } +.monaco-editor .dirty-diff-modified.pattern.primary, +.monaco-editor .dirty-diff-modified.pattern.primary:before { + background-image: linear-gradient(-45deg, var(--vscode-editorGutter-modifiedBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedBackground) 50%, var(--vscode-editorGutter-modifiedBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-editor .dirty-diff-modified.pattern.secondary, +.monaco-editor .dirty-diff-modified.pattern.secondary:before { + background-image: linear-gradient(45deg, var(--vscode-editorGutter-modifiedSecondaryBackground) 25%, var(--vscode-editorGutter-background) 25%, var(--vscode-editorGutter-background) 50%, var(--vscode-editorGutter-modifiedSecondaryBackground) 50%, var(--vscode-editorGutter-modifiedSecondaryBackground) 75%, var(--vscode-editorGutter-background) 75%, var(--vscode-editorGutter-background)); +} + +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-added, +.monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-modified { + transition: opacity 0.5s; +} + .monaco-editor .margin:hover .dirty-diff-added, -.monaco-editor .margin:hover .dirty-diff-added-pattern, -.monaco-editor .margin:hover .dirty-diff-modified, -.monaco-editor .margin:hover .dirty-diff-modified-pattern { +.monaco-editor .margin:hover .dirty-diff-modified { opacity: 1; } @@ -87,10 +121,17 @@ z-index: 9; border-top: 4px solid transparent; border-bottom: 4px solid transparent; - border-left: 4px solid var(--vscode-editorGutter-deletedBackground); pointer-events: none; } +.monaco-editor .dirty-diff-deleted.primary:after { + border-left: 4px solid var(--vscode-editorGutter-deletedBackground); +} + +.monaco-editor .dirty-diff-deleted.secondary:after { + border-left: 4px solid var(--vscode-editorGutter-deletedSecondaryBackground); +} + .monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted:after { transition: border-top-width 80ms linear, border-bottom-width 80ms linear, bottom 80ms linear, opacity 0.5s; } @@ -102,6 +143,14 @@ bottom: 0; } +.monaco-editor .dirty-diff-deleted.primary:before { + background: var(--vscode-editorGutter-deletedBackground); +} + +.monaco-editor .dirty-diff-deleted.secondary:before { + background: var(--vscode-editorGutter-deletedSecondaryBackground); +} + .monaco-workbench:not(.reduce-motion) .monaco-editor .dirty-diff-deleted:before { transition: height 80ms linear; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index ac52c163d58..a4d035a6230 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -13,7 +13,7 @@ import { ModelDecorationOptions } from '../../../../editor/common/model/textMode import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition } from '../../../../editor/common/model.js'; +import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition, IModelDeltaDecoration } from '../../../../editor/common/model.js'; import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ChangeType, getChangeType, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; @@ -58,10 +58,15 @@ class QuickDiffDecorator extends Disposable { } private addedOptions: ModelDecorationOptions; + private addedSecondaryOptions: ModelDecorationOptions; private addedPatternOptions: ModelDecorationOptions; + private addedSecondaryPatternOptions: ModelDecorationOptions; private modifiedOptions: ModelDecorationOptions; + private modifiedSecondaryOptions: ModelDecorationOptions; private modifiedPatternOptions: ModelDecorationOptions; + private modifiedSecondaryPatternOptions: ModelDecorationOptions; private deletedOptions: ModelDecorationOptions; + private deletedSecondaryOptions: ModelDecorationOptions; private decorationsCollection: IEditorDecorationsCollection | undefined; constructor( @@ -77,37 +82,38 @@ class QuickDiffDecorator extends Disposable { const minimap = decorations === 'all' || decorations === 'minimap'; const diffAdded = nls.localize('diffAdded', 'Added lines'); - this.addedOptions = QuickDiffDecorator.createDecoration('dirty-diff-added', diffAdded, { + const diffAddedOptions = { gutter, overview: { active: overview, color: overviewRulerAddedForeground }, minimap: { active: minimap, color: minimapGutterAddedBackground }, isWholeLine: true - }); - this.addedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, { - gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, - isWholeLine: true - }); + }; + this.addedOptions = QuickDiffDecorator.createDecoration('dirty-diff-added primary', diffAdded, diffAddedOptions); + this.addedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added primary pattern', diffAdded, diffAddedOptions); + this.addedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-added secondary', diffAdded, diffAddedOptions); + this.addedSecondaryPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added secondary pattern', diffAdded, diffAddedOptions); + const diffModified = nls.localize('diffModified', 'Changed lines'); - this.modifiedOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified', diffModified, { + const diffModifiedOptions = { gutter, overview: { active: overview, color: overviewRulerModifiedForeground }, minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true - }); - this.modifiedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, { - gutter, - overview: { active: overview, color: overviewRulerModifiedForeground }, - minimap: { active: minimap, color: minimapGutterModifiedBackground }, - isWholeLine: true - }); - this.deletedOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), { + }; + this.modifiedOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified primary', diffModified, diffModifiedOptions); + this.modifiedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified primary pattern', diffModified, diffModifiedOptions); + this.modifiedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified secondary', diffModified, diffModifiedOptions); + this.modifiedSecondaryPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified secondary pattern', diffModified, diffModifiedOptions); + + const diffDeleted = nls.localize('diffDeleted', 'Removed lines'); + const diffDeletedOptions = { gutter, overview: { active: overview, color: overviewRulerDeletedForeground }, minimap: { active: minimap, color: minimapGutterDeletedBackground }, isWholeLine: false - }); + }; + this.deletedOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted primary', diffDeleted, diffDeletedOptions); + this.deletedSecondaryOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted secondary', diffDeleted, diffDeletedOptions); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { @@ -123,44 +129,66 @@ class QuickDiffDecorator extends Disposable { return; } - const visibleQuickDiffs = this.quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); - const decorations = this.quickDiffModelRef.object.changes - .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) - .map((labeledChange) => { - const change = labeledChange.change; - const changeType = getChangeType(change); - const startLineNumber = change.modifiedStartLineNumber; - const endLineNumber = change.modifiedEndLineNumber || startLineNumber; + const primaryQuickDiff = this.quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const primaryQuickDiffChanges = this.quickDiffModelRef.object.changes.filter(labeledChange => labeledChange.label === primaryQuickDiff?.label); - switch (changeType) { - case ChangeType.Add: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.added ? this.addedPatternOptions : this.addedOptions - }; - case ChangeType.Delete: - return { - range: { - startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, - endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE - }, - options: this.deletedOptions - }; - case ChangeType.Modify: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions - }; - } - }); + const decorations: IModelDeltaDecoration[] = []; + for (const change of this.quickDiffModelRef.object.changes) { + const quickDiff = this.quickDiffModelRef.object.quickDiffs + .find(quickDiff => quickDiff.label === change.label); + + if (!quickDiff?.visible) { + // Not visible + continue; + } + + if (quickDiff.kind !== 'primary' && primaryQuickDiffChanges.some(c => c.change2.modified.overlapOrTouch(change.change2.modified))) { + // Overlap with primary quick diff changes + continue; + } + + const changeType = getChangeType(change.change); + const startLineNumber = change.change.modifiedStartLineNumber; + const endLineNumber = change.change.modifiedEndLineNumber || startLineNumber; + + switch (changeType) { + case ChangeType.Add: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? pattern.added ? this.addedPatternOptions : this.addedOptions + : pattern.added ? this.addedSecondaryPatternOptions : this.addedSecondaryOptions + }); + break; + case ChangeType.Delete: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, + endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? this.deletedOptions + : this.deletedSecondaryOptions + }); + break; + case ChangeType.Modify: + decorations.push({ + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: quickDiff.kind === 'primary' || quickDiff.kind === 'contributed' + ? pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions + : pattern.modified ? this.modifiedSecondaryPatternOptions : this.modifiedSecondaryOptions + }); + break; + } + } if (!this.decorationsCollection) { this.decorationsCollection = this.codeEditor.createDecorationsCollection(decorations); @@ -257,16 +285,14 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben .monaco-editor .dirty-diff-modified { border-left-width:${state.width}px; } - .monaco-editor .dirty-diff-added-pattern, - .monaco-editor .dirty-diff-added-pattern:before, - .monaco-editor .dirty-diff-modified-pattern, - .monaco-editor .dirty-diff-modified-pattern:before { + .monaco-editor .dirty-diff-added.pattern, + .monaco-editor .dirty-diff-added.pattern:before, + .monaco-editor .dirty-diff-modified.pattern, + .monaco-editor .dirty-diff-modified.pattern:before { background-size: ${state.width}px ${state.width}px; } .monaco-editor .dirty-diff-added, - .monaco-editor .dirty-diff-added-pattern, .monaco-editor .dirty-diff-modified, - .monaco-editor .dirty-diff-modified-pattern, .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index a138a78aebd..b5b24d2f8bf 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -245,21 +245,50 @@ export class QuickDiffModel extends Disposable { return Promise.resolve({ changes: [], mapChanges: new Map() }); // disposed } - const filteredToDiffable = originalURIs.filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); - if (filteredToDiffable.length === 0) { + const quickDiffs = originalURIs + .filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); + if (quickDiffs.length === 0) { return Promise.resolve({ changes: [], mapChanges: new Map() }); // All files are too large } + const quickDiffPrimary = quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + const ignoreTrimWhitespaceSetting = this.configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); const ignoreTrimWhitespace = ignoreTrimWhitespaceSetting === 'inherit' ? this.configurationService.getValue('diffEditor.ignoreTrimWhitespace') : ignoreTrimWhitespaceSetting !== 'false'; const allDiffs: QuickDiffChange[] = []; - for (const quickDiff of filteredToDiffable) { + for (const quickDiff of quickDiffs) { const diff = await this._diff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); if (diff.changes && diff.changes2 && diff.changes.length === diff.changes2.length) { for (let index = 0; index < diff.changes.length; index++) { + const change2 = diff.changes2[index]; + + // The secondary diffs are complimentary to the primary diffs, and + // they overlap. We need to remove the secondary quick diffs that + // overlap with primary quick diffs that are already in the array. + if (quickDiffPrimary && quickDiff.kind === 'secondary') { + // Check whether the: + // 1. the modified line range is equal + // 2. the original line range length is equal + const primaryQuickDiffChange = allDiffs + .find(d => d.change2.modified.equals(change2.modified) && + d.change2.original.length === change2.original.length); + + if (primaryQuickDiffChange) { + // Check whether the original content matches + const primaryModel = this._originalEditorModels.get(quickDiffPrimary.originalResource)?.textEditorModel; + const primaryContent = primaryModel?.getValueInRange(primaryQuickDiffChange.change2.toRangeMapping().originalRange); + + const secondaryModel = this._originalEditorModels.get(quickDiff.originalResource)?.textEditorModel; + const secondaryContent = secondaryModel?.getValueInRange(change2.toRangeMapping().originalRange); + if (primaryContent === secondaryContent) { + continue; + } + } + } + allDiffs.push({ label: quickDiff.label, original: quickDiff.originalResource, @@ -270,6 +299,7 @@ export class QuickDiffModel extends Disposable { } } } + const sorted = allDiffs.sort((a, b) => compareChanges(a.change, b.change)); const map: Map = new Map(); for (let i = 0; i < sorted.length; i++) { @@ -368,44 +398,37 @@ export class QuickDiffModel extends Disposable { } findNextClosestChange(lineNumber: number, inclusive = true, provider?: string): number { - let preferredProvider: string | undefined; - if (!provider && inclusive) { - preferredProvider = this.quickDiffs.find(value => value.isSCM)?.label; + const visibleQuickDiffLabels = this.quickDiffs + .filter(quickDiff => (!provider || quickDiff.label === provider) && quickDiff.visible) + .map(quickDiff => quickDiff.label); + + if (!inclusive) { + // Next visible change + const nextChange = this.changes + .findIndex(change => visibleQuickDiffLabels.includes(change.label) && + change.change.modifiedStartLineNumber > lineNumber); + + return nextChange !== -1 ? nextChange : 0; } - const possibleChanges: number[] = []; - for (let i = 0; i < this.changes.length; i++) { - if (provider && this.changes[i].label !== provider) { - continue; - } + const primaryQuickDiffLabel = this.quickDiffs + .find(quickDiff => quickDiff.kind === 'primary')?.label; - // Skip quick diffs that are not visible - if (!this.quickDiffs.find(quickDiff => quickDiff.label === this.changes[i].label)?.visible) { - continue; - } + const primaryInclusiveChangeIndex = this.changes + .findIndex(change => change.label === primaryQuickDiffLabel && + change.change.modifiedStartLineNumber <= lineNumber && + getModifiedEndLineNumber(change.change) >= lineNumber); - const change = this.changes[i]; - const possibleChangesLength = possibleChanges.length; - - if (inclusive) { - if (getModifiedEndLineNumber(change.change) >= lineNumber) { - if (preferredProvider && change.label !== preferredProvider) { - possibleChanges.push(i); - } else { - return i; - } - } - } else { - if (change.change.modifiedStartLineNumber > lineNumber) { - return i; - } - } - if ((possibleChanges.length > 0) && (possibleChanges.length === possibleChangesLength)) { - return possibleChanges[0]; - } + if (primaryInclusiveChangeIndex !== -1) { + return primaryInclusiveChangeIndex; } - return possibleChanges.length > 0 ? possibleChanges[0] : 0; + const inclusiveChangeIndex = this.changes + .findIndex(change => visibleQuickDiffLabels.includes(change.label) && + change.change.modifiedStartLineNumber <= lineNumber && + getModifiedEndLineNumber(change.change) >= lineNumber); + + return inclusiveChangeIndex !== -1 ? inclusiveChangeIndex : 0; } findPreviousClosestChange(lineNumber: number, inclusive = true, provider?: string): number { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index f22648c7169..c505713f34a 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -207,6 +207,7 @@ class QuickDiffWidget extends PeekViewWidget { const labeledChange = this.model.changes[index]; const change = labeledChange.change; this._index = index; + this.contextKeyService.createKey('originalResource', this.model.changes[index].original.toString()); this.contextKeyService.createKey('originalResourceScheme', this.model.changes[index].original.scheme); this.updateActions(); @@ -294,7 +295,7 @@ class QuickDiffWidget extends PeekViewWidget { } } let closestLesserIndex = this._index > 0 ? this._index - 1 : this.model.changes.length - 1; - for (let i = closestLesserIndex; i !== this._index; i >= 0 ? i-- : i = this.model.changes.length - 1) { + for (let i = closestLesserIndex; i !== this._index; i > 0 ? i-- : i = this.model.changes.length - 1) { if (this.model.changes[i].label === newProvider) { closestLesserIndex = i; break; @@ -307,12 +308,15 @@ class QuickDiffWidget extends PeekViewWidget { } private shouldUseDropdown(): boolean { - const visibleQuickDiffs = this.model.quickDiffs.filter(quickDiff => quickDiff.visible); - const visibleQuickDiffResults = this.model.getQuickDiffResults() - .filter(result => visibleQuickDiffs.some(quickDiff => quickDiff.label === result.label)); + const change = this.model.changes[this._index]; + const quickDiffWithChange = this.model.changes + .filter(c => change.change2.modified.overlapOrTouch(c.change2.modified)) + .map(c => c.label); - return visibleQuickDiffResults - .filter(quickDiff => quickDiff.changes.length > 0).length > 1; + const quickDiffs = this.model.quickDiffs + .filter(quickDiff => quickDiff.visible && quickDiffWithChange.includes(quickDiff.label)); + + return quickDiffs.length > 1; } private updateActions(): void { diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 60dd34b399e..a0f0d50e789 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -16,7 +16,8 @@ import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; import { Color } from '../../../../base/common/color.js'; import { darken, editorBackground, editorForeground, listInactiveSelectionBackground, opaque, - editorErrorForeground, registerColor, transparent + editorErrorForeground, registerColor, transparent, + lighten } from '../../../../platform/theme/common/colorRegistry.js'; export const IQuickDiffService = createDecorator('quickDiff'); @@ -25,13 +26,24 @@ const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackg dark: '#1B81A8', light: '#2090D3', hcDark: '#1B81A8', hcLight: '#2090D3' }, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); +registerColor('editorGutter.modifiedSecondaryBackground', + { dark: darken(editorGutterModifiedBackground, 0.5), light: lighten(editorGutterModifiedBackground, 0.9), hcDark: '#1B81A8', hcLight: '#2090D3' }, + nls.localize('editorGutterModifiedSecondaryBackground', "Editor gutter secondary background color for lines that are modified.")); + const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { dark: '#487E02', light: '#48985D', hcDark: '#487E02', hcLight: '#48985D' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); +registerColor('editorGutter.addedSecondaryBackground', + { dark: darken(editorGutterAddedBackground, 0.5), light: lighten(editorGutterAddedBackground, 0.9), hcDark: '#487E02', hcLight: '#48985D' }, + nls.localize('editorGutterAddedSecondaryBackground', "Editor gutter secondary background color for lines that are added.")); + const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); +registerColor('editorGutter.deletedSecondaryBackground', + { dark: darken(editorGutterDeletedBackground, 0.4), light: lighten(editorGutterDeletedBackground, 0.3), hcDark: '#F48771', hcLight: '#B5200D' }, + nls.localize('editorGutterDeletedSecondaryBackground', "Editor gutter secondary background color for lines that are deleted.")); export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); @@ -58,16 +70,16 @@ export interface QuickDiffProvider { label: string; rootUri: URI | undefined; selector?: LanguageSelector; - isSCM: boolean; visible: boolean; + readonly kind: 'primary' | 'secondary' | 'contributed'; getOriginalResource(uri: URI): Promise; } export interface QuickDiff { label: string; originalResource: URI; - isSCM: boolean; visible: boolean; + readonly kind: 'primary' | 'secondary' | 'contributed'; } export interface QuickDiffChange { diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 5b872ae5722..a4f211541b3 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -25,7 +25,16 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr const bIsParent = isEqualOrParent(uri, b.rootUri!); if (aIsParent && bIsParent) { - return a.rootUri!.fsPath.length - b.rootUri!.fsPath.length; + if (a.kind === 'primary') { + return -1; + } else if (b.kind === 'primary') { + return 1; + } else if (a.kind === 'secondary') { + return -1; + } else if (b.kind === 'secondary') { + return 1; + } + return 0; } else if (aIsParent) { return -1; } else if (bIsParent) { @@ -58,8 +67,8 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { }; } - private isQuickDiff(diff: { originalResource?: URI; label?: string; isSCM?: boolean }): diff is QuickDiff { - return !!diff.originalResource && (typeof diff.label === 'string') && (typeof diff.isSCM === 'boolean'); + private isQuickDiff(diff: { originalResource?: URI; label?: string }): diff is QuickDiff { + return !!diff.originalResource && (typeof diff.label === 'string'); } async getQuickDiffs(uri: URI, language: string = '', isSynchronized: boolean = false): Promise { @@ -67,17 +76,19 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { .filter(provider => !provider.rootUri || this.uriIdentityService.extUri.isEqualOrParent(uri, provider.rootUri)) .sort(createProviderComparer(uri)); - const diffs = await Promise.all(providers.map(async provider => { + const quickDiffs = await Promise.all(providers.map(async provider => { const scoreValue = provider.selector ? score(provider.selector, uri, language, isSynchronized, undefined, undefined) : 10; - const diff: Partial = { - originalResource: scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined, + const originalResource = scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined; + + return { + originalResource, label: provider.label, - isSCM: provider.isSCM, - visible: provider.visible - }; - return diff; + visible: provider.visible, + kind: provider.kind + } satisfies Partial; })); - return diffs.filter(this.isQuickDiff); + + return quickDiffs.filter(this.isQuickDiff); } } diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index ddab0a2e10b..deccc655fb0 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -186,6 +186,18 @@ color: var(--vscode-textLink-activeForeground); } +.search-view .message .keyword-refresh { + vertical-align: sub; + margin-right: 4px; + cursor: pointer; +} + +.search-view .message .keyword-refresh:hover, +.search-view .message .keyword-refresh:active { + color: var(--vscode-textLink-activeForeground); +} + + .search-view .foldermatch, .search-view .filematch { display: flex; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 2a7f4d3c326..102e8a447d6 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -72,7 +72,7 @@ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/edit import { IPreferencesService, ISettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; import { ITextQueryBuilderOptions, QueryBuilder } from '../../../services/search/common/queryBuilder.js'; import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; -import { TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; +import { AISearchKeyword, TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -1820,7 +1820,11 @@ export class SearchView extends ViewPane { const result = this.viewModel.addAIResults(); return result.then((complete) => { clearTimeout(slowTimer); - this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); + if (complete.aiKeywords && complete.aiKeywords.length > 0) { + this.updateKeywordSuggestion(complete.aiKeywords); + } else { + this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); + } return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete, false); }, (e) => { clearTimeout(slowTimer); @@ -1948,6 +1952,51 @@ export class SearchView extends ViewPane { } } + private handleKeywordClick(keyword: string) { + this.searchWidget.searchInput?.setValue(keyword); + this.triggerQueryChange({ preserveFocus: false, triggeredOnType: false, shouldKeepAIResults: false }); + } + + private updateKeywordSuggestion(keywords: AISearchKeyword[]) { + let currentKeyword = keywords.shift()?.keyword || ''; + const messageEl = this.clearMessage(); + + // Refresh icon + if (keywords.length !== 0) { + const icon = dom.append(messageEl, dom.$('')); + icon.ariaLabel = nls.localize('search.refresh', "Get new suggestion"); + icon.role = 'button'; + icon.tabIndex = 0; + icon.classList.add('codicon', 'codicon-refresh', 'keyword-refresh'); + icon.onclick = () => { + // change the keyword to the next one + const nextKeyword = keywords.shift(); + if (nextKeyword) { + currentKeyword = nextKeyword.keyword; + textButton.element.textContent = currentKeyword; + } + if (keywords.length === 0) { + icon.remove(); + } + }; + } + + // Unclickable message + const resultMsg = nls.localize('keywordSuggestion.message', "Search instead for: "); + this.tree.ariaLabel = resultMsg + nls.localize('aiSearchForTerm', " - Search: {0}", currentKeyword); + dom.append(messageEl, resultMsg); + + const textButton = this.messageDisposables.add(new SearchLinkButton( + currentKeyword, + () => this.handleKeywordClick(currentKeyword), + this.hoverService, + )); + + dom.append(messageEl, textButton.element); + + + } + private addMessage(message: TextSearchCompleteMessage) { const messageBox = this.messagesElement.firstChild as HTMLDivElement; if (!messageBox) { return; } diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index e11f2203cda..84a3c98a421 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -129,8 +129,6 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider { this._lastTask = this._currentTask; }); return executeResult; } catch (error) { + this._removeFromActiveTasks(task); if (error instanceof TaskError) { throw error; } else if (error instanceof Error) { @@ -528,6 +534,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (!taskResult) { const activeTask = this._activeTasks[dependencyTask.getMapKey()] ?? this._getInstances(dependencyTask).pop(); taskResult = activeTask && this._getDependencyPromise(activeTask); + this._activeTasks[mapKey].terminal = activeTask?.terminal; + this._activeTasks[mapKey].count = { count: task.configurationProperties.dependsOn.length }; } } if (!taskResult) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 851fb136c3c..2059d0d68b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -910,7 +910,7 @@ export interface ITerminalInstance extends IBaseTerminalInstance { */ sendPath(originalPath: string | URI, shouldExecute: boolean): Promise; - runCommand(command: string, shouldExecute?: boolean): void; + runCommand(command: string, shouldExecute?: boolean): Promise; /** * Takes a path and returns the properly escaped path to send to a given shell. On Windows, this diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index b835c0b5c52..44c8a117b9b 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -225,6 +225,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach fastScrollModifier: 'alt', fastScrollSensitivity: config.fastScrollSensitivity, scrollSensitivity: config.mouseWheelScrollSensitivity, + scrollOnEraseInDisplay: true, wordSeparator: config.wordSeparators, overviewRuler: { width: 14, diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index c9266aad1e5..7f6f5bdab7c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -18,6 +18,9 @@ bash_major_version=${BASH_VERSINFO[0]} __vscode_shell_env_reporting="$VSCODE_SHELL_ENV_REPORTING" unset VSCODE_SHELL_ENV_REPORTING +envVarsToReport=() +IFS=',' read -ra envVarsToReport <<< "$__vscode_shell_env_reporting" + if (( BASH_VERSINFO[0] >= 4 )); then use_associative_array=1 # Associative arrays are only available in bash 4.0+ @@ -63,8 +66,8 @@ fi if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do - VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" + VARNAME="${ITEM%%=*}" + VALUE="${ITEM#*=}" export $VARNAME="$VALUE" done builtin unset VSCODE_ENV_REPLACE @@ -72,8 +75,8 @@ fi if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do - VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" + VARNAME="${ITEM%%=*}" + VALUE="${ITEM#*=}" export $VARNAME="$VALUE${!VARNAME}" done builtin unset VSCODE_ENV_PREPEND @@ -81,8 +84,8 @@ fi if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do - VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2-)" + VARNAME="${ITEM%%=*}" + VALUE="${ITEM#*=}" export $VARNAME="${!VARNAME}$VALUE" done builtin unset VSCODE_ENV_APPEND @@ -248,26 +251,6 @@ __updateEnvCacheAA() { fi } -__trackMissingEnvVarsAA() { - if [ "$use_associative_array" = 1 ]; then - declare -A currentEnvMap - while IFS= read -r line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" - currentEnvMap["$key"]="$value" - fi - done < <(env) - - for key in "${!vsc_aa_env[@]}"; do - if [ -z "${currentEnvMap[$key]}" ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "$key" "$(__vsc_escape_value "${vsc_aa_env[$key]}")" "$__vsc_nonce" - builtin unset "vsc_aa_env[$key]" - fi - done - fi -} - __updateEnvCache() { local key="$1" local value="$2" @@ -287,86 +270,51 @@ __updateEnvCache() { builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" } -__trackMissingEnvVars() { - local current_env_keys=() - - while IFS='=' read -r key value; do - current_env_keys+=("$key") - done < <(env) - - # Compare vsc_env_keys with user's current_env_keys - for key in "${vsc_env_keys[@]}"; do - local found=0 - for env_key in "${current_env_keys[@]}"; do - if [[ "$key" == "$env_key" ]]; then - found=1 - break - fi - done - if [ "$found" = 0 ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${vsc_env_keys[i]}" "$(__vsc_escape_value "${vsc_env_values[i]}")" "$__vsc_nonce" - builtin unset 'vsc_env_keys[i]' - builtin unset 'vsc_env_values[i]' - fi - done - - # Remove gaps from unset - vsc_env_keys=("${vsc_env_keys[@]}") - vsc_env_values=("${vsc_env_values[@]}") -} - __vsc_update_env() { - if [[ "$__vscode_shell_env_reporting" == "1" ]]; then + if [[ ${#envVarsToReport[@]} -gt 0 ]]; then builtin printf '\e]633;EnvSingleStart;%s;%s\a' 0 $__vsc_nonce if [ "$use_associative_array" = 1 ]; then if [ ${#vsc_aa_env[@]} -eq 0 ]; then # Associative array is empty, do not diff, just add - # Use null byte instead of a newline to support multi-line values (e.g. PS1 values) - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - # %% removes longest match of =* Ensure we get everything before first equal sign. - local key="${line%%=*}" - # # removes shortest match of *= Ensure we get everything after first equal sign. Preserving additional equal signs. - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" vsc_aa_env["$key"]="$value" builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" fi - done < <(env -0) # env command with null bytes as separator instead of newlines + done else # Diff approach for associative array - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" __updateEnvCacheAA "$key" "$value" fi - done < <(env -0) - __trackMissingEnvVarsAA + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi else if [[ -z ${vsc_env_keys[@]} ]] && [[ -z ${vsc_env_values[@]} ]]; then # Non associative arrays are both empty, do not diff, just add - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" vsc_env_keys+=("$key") vsc_env_values+=("$value") builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" fi - done < <(env -0) + done else # Diff approach for non-associative arrays - while IFS= read -r -d $'\0' line; do - if [[ "$line" == *"="* ]]; then - local key="${line%%=*}" - local value="${line#*=}" + for key in "${envVarsToReport[@]}"; do + if [ -n "${!key+x}" ]; then + local value="${!key}" __updateEnvCache "$key" "$value" fi - done < <(env -0) - __trackMissingEnvVars + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi fi builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index ba1b26b6126..74536cbd328 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -113,6 +113,9 @@ unset VSCODE_NONCE __vscode_shell_env_reporting="$VSCODE_SHELL_ENV_REPORTING" unset VSCODE_SHELL_ENV_REPORTING +envVarsToReport=() +IFS=',' read -rA envVarsToReport <<< "$__vscode_shell_env_reporting" + builtin printf "\e]633;P;ContinuationPrompt=%s\a" "$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')" # Report prompt type @@ -150,23 +153,6 @@ __update_env_cache_aa() { fi } -__track_missing_env_vars_aa() { - if [ $__vsc_use_aa -eq 1 ]; then - typeset -A currentEnvMap - while IFS='=' read -r key value; do - currentEnvMap["$key"]="$value" - done < <(env) - - for k in "${(@k)vsc_aa_env}"; do - # if currentEnvMap does not have the key, then it is missing - if ! [[ -v currentEnvMap[$k] ]]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${(Q)k}" "$(__vsc_escape_value "${vsc_aa_env[$k]}")" "$__vsc_nonce" - builtin unset "vsc_aa_env[$k]" - fi - done - fi -} - __update_env_cache() { local key="$1" local value="$2" @@ -187,69 +173,49 @@ __update_env_cache() { builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" } -__track_missing_env_vars() { - local currentEnvKeys=(); - - while IFS='=' read -r key value; do - currentEnvKeys+=("$key"); - done < <(env); - - # Compare __vsc_env_keys with user's currentEnvKeys - for ((i = 1; i <= ${#__vsc_env_keys[@]}; i++)); do - local found=0; - for envKey in "${currentEnvKeys[@]}"; do - if [[ "${__vsc_env_keys[$i]}" == "$envKey" ]]; then - found=1; - break; - fi; - done; - if [ "$found" = 0 ]; then - builtin printf '\e]633;EnvSingleDelete;%s;%s;%s\a' "${__vsc_env_keys[$i]}" "$(__vsc_escape_value "${__vsc_env_values[$i]}")" "$__vsc_nonce"; - unset "__vsc_env_keys[$i]"; - unset "__vsc_env_values[$i]"; - fi; - done; - - # Remove gaps from unset - __vsc_env_keys=("${(@)__vsc_env_keys}"); - __vsc_env_values=("${(@)__vsc_env_values}"); -} - - __vsc_update_env() { - if [[ "$__vscode_shell_env_reporting" == "1" ]]; then + if [[ ${#envVarsToReport[@]} -gt 0 ]]; then builtin printf '\e]633;EnvSingleStart;%s;%s;\a' 0 $__vsc_nonce if [ $__vsc_use_aa -eq 1 ]; then if [[ ${#vsc_aa_env[@]} -eq 0 ]]; then # Associative array is empty, do not diff, just add - while IFS='=' read -r key value; do - vsc_aa_env["$key"]="$value" - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + for key in "${envVarsToReport[@]}"; do + if [[ -v $key ]]; then + vsc_aa_env["$key"]="${(P)key}" + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "${(P)key}")" "$__vsc_nonce" + fi + done else # Diff approach for associative array - while IFS='=' read -r key value; do - __update_env_cache_aa "$key" "$value" - done < <(env) - __track_missing_env_vars_aa - + for var in "${envVarsToReport[@]}"; do + if [[ -v $var ]]; then + value="${(P)var}" + __update_env_cache_aa "$var" "$value" + fi + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi else # Two arrays approach if [[ ${#__vsc_env_keys[@]} -eq 0 ]] && [[ ${#__vsc_env_values[@]} -eq 0 ]]; then # Non-associative arrays are both empty, do not diff, just add - while IFS='=' read -r key value; do - __vsc_env_keys+=("$key") - __vsc_env_values+=("$value") - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + for key in "${envVarsToReport[@]}"; do + if [[ -v $key ]]; then + value="${(P)key}" + __vsc_env_keys+=("$key") + __vsc_env_values+=("$value") + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" + fi + done else # Diff approach for non-associative arrays - while IFS='=' read -r key value; do - __update_env_cache "$key" "$value" - done < <(env) - __track_missing_env_vars - + for var in "${envVarsToReport[@]}"; do + if [[ -v $var ]]; then + value="${(P)var}" + __update_env_cache "$var" "$value" + fi + done + # Track missing env vars not needed for now, as we are only tracking pre-defined env var from terminalEnvironment. fi fi diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index b43a0117468..9df1ecd8a3f 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -23,6 +23,10 @@ or exit set --global VSCODE_SHELL_INTEGRATION 1 set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING set -e VSCODE_SHELL_ENV_REPORTING +set -g envVarsToReport +if test -n "$__vscode_shell_env_reporting" + set envVarsToReport (string split "," "$__vscode_shell_env_reporting") +end # Apply any explicit path prefix (see #99878) # On fish, '$fish_user_paths' is always prepended to the PATH, for both login and non-login shells, so we need @@ -159,15 +163,20 @@ function __vsc_update_cwd --on-event fish_prompt end end -if test "$__vscode_shell_env_reporting" = "1" +if test -n "$__vscode_shell_env_reporting" function __vsc_update_env --on-event fish_prompt - __vsc_esc EnvSingleStart 1 - for line in (env) - set myVar (echo $line | awk -F= '{print $1}') - set myVal (echo $line | awk -F= '{print $2}') - __vsc_esc EnvSingleEntry $myVar (__vsc_escape_value "$myVal") + if test (count $envVarsToReport) -gt 0 + __vsc_esc EnvSingleStart 1 + + for key in $envVarsToReport + if set -q $key + set -l value $$key + __vsc_esc EnvSingleEntry $key (__vsc_escape_value "$value") + end + end + + __vsc_esc EnvSingleEnd end - __vsc_esc EnvSingleEnd end end diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 5695d50e874..c29140f2ea9 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -27,6 +27,10 @@ $env:VSCODE_STABLE = $null $__vscode_shell_env_reporting = $env:VSCODE_SHELL_ENV_REPORTING $env:VSCODE_SHELL_ENV_REPORTING = $null +$Global:envVarsToReport = @() +if ($__vscode_shell_env_reporting) { + $Global:envVarsToReport = $__vscode_shell_env_reporting.Split(',') +} $osVersion = [System.Environment]::OSVersion.Version $isWindows10 = $IsWindows -and $osVersion.Major -eq 10 -and $osVersion.Minor -eq 0 -and $osVersion.Build -lt 22000 @@ -97,11 +101,15 @@ function Global:Prompt() { # Send current environment variables as JSON # OSC 633 ; EnvJson ; ; - if ($__vscode_shell_env_reporting -eq "1") { + if ($Global:envVarsToReport.Count -gt 0) { $envMap = @{} - Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } - $envJson = $envMap | ConvertTo-Json -Compress - $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" + foreach ($varName in $envVarsToReport) { + if (Test-Path "env:$varName") { + $envMap[$varName] = (Get-Item "env:$varName").Value + } + } + $envJson = $envMap | ConvertTo-Json -Compress + $Result += "$([char]0x1b)]633;EnvJson;$(__VSCode-Escape-Value $envJson);$Nonce`a" } # Before running the original prompt, put $? back to what it was: diff --git a/src/vs/workbench/contrib/terminalContrib/history/common/history.ts b/src/vs/workbench/contrib/terminalContrib/history/common/history.ts index 29709a50e0c..8eaac217b45 100644 --- a/src/vs/workbench/contrib/terminalContrib/history/common/history.ts +++ b/src/vs/workbench/contrib/terminalContrib/history/common/history.ts @@ -299,7 +299,8 @@ export async function fetchZshHistory(accessor: ServicesAccessor): Promise = new Set(); for (let i = 0; i < fileLines.length; i++) { const sanitized = fileLines[i].replace(/\\\n/g, '\n').trim(); diff --git a/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts b/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts index 2fffe8c043b..c9db0ef431f 100644 --- a/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts @@ -216,94 +216,117 @@ suite('Terminal history', () => { suite('fetchZshHistory', () => { let fileScheme: string; let filePath: string; - const fileContent: string = [ - ': 1655252330:0;single line command', - ': 1655252330:0;git commit -m "A wrapped line in pwsh history\\', - '\\', - 'Some commit description\\', - '\\', - 'Fixes #xyz"', - ': 1655252330:0;git status', - ': 1655252330:0;two "\\', - 'line"' - ].join('\n'); + const fileContentType = [ + { + type: 'simple', + content: [ + 'single line command', + 'git commit -m "A wrapped line in pwsh history\\', + '\\', + 'Some commit description\\', + '\\', + 'Fixes #xyz"', + 'git status', + 'two "\\', + 'line"' + ].join('\n') + }, + { + type: 'extended', + content: [ + ': 1655252330:0;single line command', + ': 1655252330:0;git commit -m "A wrapped line in pwsh history\\', + '\\', + 'Some commit description\\', + '\\', + 'Fixes #xyz"', + ': 1655252330:0;git status', + ': 1655252330:0;two "\\', + 'line"' + ].join('\n') + }, + ]; let instantiationService: TestInstantiationService; let remoteConnection: Pick | null = null; let remoteEnvironment: Pick | null = null; - setup(() => { - instantiationService = new TestInstantiationService(); - instantiationService.stub(IFileService, { - async readFile(resource: URI) { - const expected = URI.from({ scheme: fileScheme, path: filePath }); - strictEqual(resource.scheme, expected.scheme); - strictEqual(resource.path, expected.path); - return { value: VSBuffer.fromString(fileContent) }; - } - } as Pick); - instantiationService.stub(IRemoteAgentService, { - async getEnvironment() { return remoteEnvironment; }, - getConnection() { return remoteConnection; } - } as Pick); - }); - - teardown(() => { - instantiationService.dispose(); - }); - - if (!isWindows) { - suite('local', () => { - let originalEnvValues: { HOME: string | undefined }; + for (const { type, content } of fileContentType) { + suite(type, () => { setup(() => { - originalEnvValues = { HOME: env['HOME'] }; - env['HOME'] = '/home/user'; - remoteConnection = { remoteAuthority: 'some-remote' }; - fileScheme = Schemas.vscodeRemote; - filePath = '/home/user/.bash_history'; + instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, { + async readFile(resource: URI) { + const expected = URI.from({ scheme: fileScheme, path: filePath }); + strictEqual(resource.scheme, expected.scheme); + strictEqual(resource.path, expected.path); + return { value: VSBuffer.fromString(content) }; + } + } as Pick); + instantiationService.stub(IRemoteAgentService, { + async getEnvironment() { return remoteEnvironment; }, + getConnection() { return remoteConnection; } + } as Pick); }); + teardown(() => { - if (originalEnvValues['HOME'] === undefined) { - delete env['HOME']; - } else { - env['HOME'] = originalEnvValues['HOME']; - } + instantiationService.dispose(); }); - test('current OS', async () => { - filePath = '/home/user/.zsh_history'; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + + if (!isWindows) { + suite('local', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.bash_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('current OS', async () => { + filePath = '/home/user/.zsh_history'; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); + }); + } + suite('remote', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.zsh_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('Windows', async () => { + remoteEnvironment = { os: OperatingSystem.Windows }; + strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined); + }); + test('macOS', async () => { + remoteEnvironment = { os: OperatingSystem.Macintosh }; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); + test('Linux', async () => { + remoteEnvironment = { os: OperatingSystem.Linux }; + deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); + }); }); }); } - suite('remote', () => { - let originalEnvValues: { HOME: string | undefined }; - setup(() => { - originalEnvValues = { HOME: env['HOME'] }; - env['HOME'] = '/home/user'; - remoteConnection = { remoteAuthority: 'some-remote' }; - fileScheme = Schemas.vscodeRemote; - filePath = '/home/user/.zsh_history'; - }); - teardown(() => { - if (originalEnvValues['HOME'] === undefined) { - delete env['HOME']; - } else { - env['HOME'] = originalEnvValues['HOME']; - } - }); - test('Windows', async () => { - remoteEnvironment = { os: OperatingSystem.Windows }; - strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined); - }); - test('macOS', async () => { - remoteEnvironment = { os: OperatingSystem.Macintosh }; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); - }); - test('Linux', async () => { - remoteEnvironment = { os: OperatingSystem.Linux }; - deepStrictEqual((await instantiationService.invokeFunction(fetchZshHistory))!.commands, expectedCommands); - }); - }); }); suite('fetchPwshHistory', () => { let fileScheme: string; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index a57f9c15dd7..a1c7dac9944 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -236,6 +236,11 @@ registerAction2(class extends Action2 { title: localize2('welcome.showAllWalkthroughs', 'Open Walkthrough...'), category, f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 3, + }, }); } @@ -286,6 +291,35 @@ registerAction2(class extends Action2 { } }); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'welcome.showNewWelcome', + title: localize2('welcome.showNewWelcome', 'Open New Welcome Experience'), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const options: GettingStartedEditorOptions = { selectedCategory: 'Setup', showNewExperience: true }; + + editorService.openEditor({ + resource: GettingStartedInput.RESOURCE, + options + }); + } +}); + +CommandsRegistry.registerCommand({ + id: 'welcome.newWorkspaceChat', + handler: (accessor, stepID: string) => { + const commandService = accessor.get(ICommandService); + commandService.executeCommand('workbench.action.chat.open', { mode: 'agent', query: '#new ', isPartialQuery: true }); + } +}); + export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | 'webworker' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote or serverless contexts may be different from the platform of the UI")); class WorkspacePlatformContribution { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 01778b2f109..2cfbd675def 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -925,13 +925,21 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (this.currentWalkthrough) { - this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + if (this.editorInput.showNewExperience === true) { + this.buildNewCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } else { + this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } this.setSlide('details'); return; } } else { - this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + if (this.editorInput.showNewExperience === true) { + this.buildNewCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } else { + this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + } this.setSlide('details'); return; } @@ -1394,6 +1402,235 @@ export class GettingStartedPage extends EditorPane { super.clearInput(); } + + // Helper method to navigate between steps + private navigateStep(direction: number, steps: IResolvedWalkthroughStep[]) { + if (!this.editorInput.selectedStep) { return; } + + const currentIndex = steps.findIndex(step => step.id === this.editorInput.selectedStep); + if (currentIndex === -1) { return; } + + const newIndex = Math.max(0, Math.min(steps.length - 1, currentIndex + direction)); + if (newIndex !== currentIndex) { + this.selectStepByIndex(newIndex, steps, direction); + } + } + + private selectStepByIndex(newIndex: number, steps: IResolvedWalkthroughStep[], direction: number) { + const currentIndex = steps.findIndex(step => step.id === this.editorInput.selectedStep); + const slidesContainer = this.stepsContent.querySelector('.step-slides-container') as HTMLElement; + + if (slidesContainer) { + // Apply the transform to move the slides + const slides = slidesContainer.querySelectorAll('.step-slide'); + + // First make all slides visible for the animation + slides.forEach((slide, index) => { + const slideElement = slide as HTMLElement; + // Position all slides in their starting positions + if (index === currentIndex) { + slideElement.style.display = 'block'; + slideElement.style.transform = 'translateX(0)'; + } else if (index === newIndex) { + slideElement.style.display = 'block'; + slideElement.style.transform = `translateX(${direction < 0 ? '-100%' : '100%'})`; + } else { + slideElement.style.display = 'none'; + } + }); + + // Force a reflow to ensure the initial positions are applied + slidesContainer.getBoundingClientRect(); + + // Now animate to the final positions + setTimeout(() => { + slides.forEach((slide, index) => { + const slideElement = slide as HTMLElement; + if (index === currentIndex) { + slideElement.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`; + setTimeout(() => { + slideElement.style.display = 'none'; + }, SLIDE_TRANSITION_TIME_MS); + } else if (index === newIndex) { + slideElement.style.transform = 'translateX(0)'; + } + }); + }, 20); + + // Update the active dot + const dots = this.stepsContent.querySelectorAll('.step-dot'); + dots.forEach((dot, index) => { + if (index === newIndex) { + dot.classList.add('active'); + } else { + dot.classList.remove('active'); + } + }); + + // Update the selected step and build its media + this.selectSlide(steps[newIndex].id); + } + } + + private buildNewCategorySlide(categoryID: string, selectedStep?: string) { + this.container.classList.add('newSlide'); + if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } + + this.detailsPageDisposables.clear(); + this.mediaDisposables.clear(); + + const category = this.gettingStartedCategories.find(category => category.id === categoryID); + if (!category) { + throw Error('could not find category with ID ' + categoryID); + } + + // Filter steps based on when context + const steps = category.steps.filter(step => this.contextService.contextMatchesRules(step.when)); + + // Create the slide container that will hold all step slides + const slidesContainer = $('.step-slides-container'); + + // Create the dots navigation + const dotsContainer = $('.step-dots-container'); + + // For each step, create a slide + steps.forEach((step, index) => { + // Create the slide + const slideElement = $('.step-slide', { 'data-step': step.id }); + + // Create the content container with flex layout + const slideContent = $('.step-slide-content'); + + // Text content column + const textContent = $('.step-text-content'); + + // Create step title + const titleElement = $('h3.step-title', { 'x-step-title-for': step.id }); + reset(titleElement, ...renderLabelWithIcons(step.title)); + textContent.appendChild(titleElement); + + // Create step description container + const descriptionContainer = $('.step-description', { 'x-step-description-for': step.id }); + this.buildMarkdownDescription(descriptionContainer, step.description); + textContent.appendChild(descriptionContainer); + + // Add buttons container if needed + const actionsContainer = $('.step-actions'); + textContent.appendChild(actionsContainer); + + // Append text content to the slide + slideContent.appendChild(textContent); + + // Add completed slide to the slides container + slideElement.appendChild(slideContent); + slidesContainer.appendChild(slideElement); + + // Create a dot for this slide in the navigation - now with click handlers + const dot = $('button.step-dot', { + 'data-step-dot-index': `${index}`, + 'aria-label': localize('goToStep', "Go to step {0}", step.title), + 'role': 'button' + }); + + if ((!selectedStep && index === 0) || (selectedStep === step.id)) { + dot.classList.add('active'); + } + + // Add event listeners directly to the dots + this.detailsPageDisposables.add(addDisposableListener(dot, 'click', () => { + const currentIndex = steps.findIndex(step => step.id === this.editorInput.selectedStep); + if (currentIndex !== index) { + const direction = index > currentIndex ? 1 : -1; + this.selectStepByIndex(index, steps, direction); + } + })); + + dotsContainer.appendChild(dot); + }); + + // Handle initial selected step + let initialStepIndex = 0; + if (selectedStep) { + initialStepIndex = steps.findIndex(step => step.id === selectedStep); + if (initialStepIndex === -1) { initialStepIndex = 0; } + } + + // Set the current walkthrough and step + this.currentWalkthrough = category; + this.editorInput.selectedCategory = categoryID; + this.editorInput.selectedStep = steps[initialStepIndex].id; + + // Search through slidesContainer for slideElement with data-step attribute matching selectedStep.id + // then fetch the slideContent and append mediaComponent to it + const selectedSlide = slidesContainer.querySelector(`.step-slide[data-step="${steps[initialStepIndex].id}"]`); + if (selectedSlide) { + const selectedSlideContent = selectedSlide.querySelector('.step-slide-content'); + this.buildMediaComponent(steps[initialStepIndex].id); + selectedSlideContent?.appendChild(this.stepMediaComponent); + } + + // Category title and description + const categoryHeader = $('.category-header'); + const categoryTitle = $('h2.category-title', { 'x-category-title-for': category.id }); + reset(categoryTitle, ...renderLabelWithIcons(category.title)); + categoryHeader.appendChild(categoryTitle); + + // Build the container for the whole slide deck + const stepsContainer = $('.getting-started-steps-container', {}, + categoryHeader, + slidesContainer, + dotsContainer + ); + + // Set up the scroll container + this.detailsScrollbar = this._register(new DomScrollableElement(stepsContainer, { className: 'steps-container' })); + const stepListComponent = this.detailsScrollbar.getDomNode(); + + // Append to the content area + reset(this.stepsContent, stepListComponent); + + // Add keyboard navigation + this.detailsPageDisposables.add(addDisposableListener(dotsContainer, 'keydown', (e) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.RightArrow) { + this.navigateStep(1, steps); + } else if (event.keyCode === KeyCode.LeftArrow) { + this.navigateStep(-1, steps); + } + })); + + // Register listeners for step selection + this.registerDispatchListeners(); + + // Add handler for the step selection event + this.detailsPageDisposables.add(this.stepDisposables); + + this.detailsScrollbar.scanDomNode(); + this.detailsPageScrollbar?.scanDomNode(); + } + + private selectSlide(stepId: string) { + this.editorInput.selectedStep = stepId; + + const step = this.currentWalkthrough?.steps.find(step => step.id === stepId); + if (!step) { return; } + + const selectedSlide = this.stepsContent.querySelector(`.step-slide[data-step="${stepId}"]`); + if (selectedSlide) { + const selectedSlideContent = selectedSlide.querySelector('.step-slide-content'); + this.mediaDisposables.clear(); + this.stepDisposables.clear(); + this.buildMediaComponent(this.editorInput.selectedStep); + selectedSlideContent?.appendChild(this.stepMediaComponent); + setTimeout(() => (selectedSlideContent as HTMLElement).focus(), 0); + + } + + this.gettingStartedService.progressByEvent('stepSelected:' + stepId); + this.detailsPageScrollbar?.scanDomNode(); + this.detailsScrollbar?.scanDomNode(); + } + private buildCategorySlide(categoryID: string, selectedStep?: string) { if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } @@ -1627,8 +1864,12 @@ export class GettingStartedPage extends EditorPane { const prevButton = this.container.querySelector('.prev-button.button-link'); prevButton!.style.display = this.editorInput.showWelcome || this.prevWalkthrough ? 'block' : 'none'; - const moreTextElement = prevButton!.querySelector('.moreText'); - moreTextElement!.textContent = firstLaunch ? localize('welcome', "Welcome") : localize('goBack', "Go Back"); + if (this.editorInput.showNewExperience) { + prevButton!.style.display = 'none'; + } else { + const moreTextElement = prevButton!.querySelector('.moreText'); + moreTextElement!.textContent = firstLaunch ? localize('welcome', "Welcome") : localize('goBack', "Go Back"); + } this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = true); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts index 7375366e5c6..d08432a2631 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts @@ -19,6 +19,7 @@ export interface GettingStartedEditorOptions extends IEditorOptions { showTelemetryNotice?: boolean; showWelcome?: boolean; walkthroughPageTitle?: string; + showNewExperience?: boolean; } export class GettingStartedInput extends EditorInput { @@ -29,6 +30,8 @@ export class GettingStartedInput extends EditorInput { private _selectedStep: string | undefined; private _showTelemetryNotice: boolean; private _showWelcome: boolean; + private _showNewExperience: boolean; + private _walkthroughPageTitle: string | undefined; override get typeId(): string { @@ -72,6 +75,7 @@ export class GettingStartedInput extends EditorInput { this._showTelemetryNotice = !!options.showTelemetryNotice; this._showWelcome = options.showWelcome ?? true; this._walkthroughPageTitle = options.walkthroughPageTitle; + this._showNewExperience = options.showNewExperience ?? false; } override getName() { @@ -118,4 +122,12 @@ export class GettingStartedInput extends EditorInput { set walkthroughPageTitle(value: string | undefined) { this._walkthroughPageTitle = value; } + + get showNewExperience(): boolean { + return this._showNewExperience; + } + + set showNewExperience(value: boolean) { + this._showNewExperience = value; + } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index cd312dfbe33..1d012e47a7b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -134,7 +134,8 @@ grid-template-areas: "left-column" "right-column" "footer"; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header, .monaco-workbench .part.editor > .content .gettingStartedContainer.height-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header { +.monaco-workbench .part.editor > .content .gettingStartedContainer.width-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header, +.monaco-workbench .part.editor > .content .gettingStartedContainer.height-constrained .gettingStartedSlideCategories > .gettingStartedCategoriesContainer > .header { display: none; } @@ -142,7 +143,8 @@ display: none; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry, .gettingStartedContainer.noExtensions { +.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .gettingStartedSlideCategories li.showWalkthroughsEntry, +.gettingStartedContainer.noExtensions { display: unset; } @@ -299,7 +301,7 @@ font-size: 16px; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content:not(:empty){ +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content:not(:empty) { margin-bottom: 8px; } @@ -360,6 +362,7 @@ position: relative; top: auto; } + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-category img.category-icon { margin-right: 10px; margin-left: 10px; @@ -583,7 +586,7 @@ grid-area: steps-start / media-start / footer-start / media-end; align-self: self-start; display: flex; - justify-content:center ; + justify-content: center; height: 100%; width: 100%; } @@ -655,7 +658,7 @@ display: inline; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .index-list.getting-started { +.monaco-workbench .part.editor > .content .gettingStartedContainer.noWalkthroughs .index-list.getting-started { display: none; } @@ -810,7 +813,8 @@ background: transparent; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .openAWalkthrough > button, .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .showOnStartup { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .openAWalkthrough > button, +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .showOnStartup { text-align: center; display: flex; justify-content: center; @@ -829,7 +833,7 @@ } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox.codicon:not(.checked)::before { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox.codicon:not(.checked)::before { opacity: 0; } @@ -867,7 +871,8 @@ line-height: 1.3em; } -.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-step .step-description-container .monaco-button, .monaco-workbench .part.editor > .content .gettingStartedContainer .max-lines-3 { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-step .step-description-container .monaco-button, +.monaco-workbench .part.editor > .content .gettingStartedContainer .max-lines-3 { /* Supported everywhere: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp#browser_compatibility */ -webkit-line-clamp: 3; display: -webkit-box; @@ -936,7 +941,7 @@ outline-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); } -.monaco-workbench .part.editor > .content .gettingStartedContainer button.expanded:hover { +.monaco-workbench .part.editor > .content .gettingStartedContainer button.expanded:hover { background: var(--vscode-welcomePage-tileBackground); } @@ -974,7 +979,7 @@ color: var(--vscode-textLink-activeForeground); } -.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.hide-category-button):active { +.monaco-workbench .part.editor > .content .gettingStartedContainer a:not(.hide-category-button):active { color: var(--vscode-textLink-activeForeground); } @@ -994,7 +999,7 @@ border: 1px solid var(--vscode-contrastBorder); } -.monaco-workbench .part.editor > .content .gettingStartedContainer button.button-link { +.monaco-workbench .part.editor > .content .gettingStartedContainer button.button-link { border: inherit; } @@ -1029,3 +1034,208 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { border-color: var(--vscode-checkbox-border) !important; } + +/* Full width layout for the new slide design */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent { + height: 100%; + width: 100%; + max-width: 100%; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +/* Back button position */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .prev-button { + padding: 16px 32px 0; + position: static; + margin: 0; +} + +/* Title area */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-category, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .category-header { + padding: 16px 32px; + border-bottom: 1px solid var(--vscode-welcomePage-tileBorder); + text-align: center; + align-self: center; + max-width: 800px; + margin: 0 auto; +} + +/* Steps container - takes most of the space */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .steps-container { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Hide the default media container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media { + display: none; +} + +/* Getting Started Steps Container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .getting-started-steps-container { + padding: 20px; + max-width: 1000px; + margin: 0 auto; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +} + +/* Step slides container */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slides-container { + display: flex; + overflow: hidden; + margin: 30px 0; + position: relative; + flex: 1; + width: 100%; + transition: transform 0.6s ease; +} + +/* Individual slide styling */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide { + min-width: 100%; + height: 100%; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + transition: transform 0.6s ease; + padding: 0 32px; +} + +/* Two-column layout for slide content */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide-content { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: "text media"; + max-width: 1200px; + width: 100%; + height: 100%; + gap: 40px; +} + +/* Left column - text content only */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content { + grid-area: text; + display: flex; + flex-direction: column; + justify-content: center; + padding-right: 16px; + min-height: 300px; + height: 100%; +} + +/* Right column - for media content */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + grid-area: media; + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + height: 100%; +} + +/* Navigation dots */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container { + display: flex; + justify-content: center; + gap: 6px; + margin: 12px 0; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot { + width: 8px; + height: 8px; + background-color: var(--vscode-button-secondaryBackground); + border: none; + border-radius: 50%; + cursor: pointer; + padding: 0; + transition: transform 0.2s ease, background-color 0.2s ease; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot:hover { + transform: scale(1.2); + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot.active { + background-color: var(--vscode-button-background); + width: 9px; + height: 9px; +} + +/* Footer area */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-footer, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .getting-started-footer { + padding: 16px 0; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + border-top: 1px solid var(--vscode-welcomePage-tileBorder); +} + +/* Step title styling */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide h3.step-title { + font-size: 1.5em; + margin: 0 0 16px 0; + padding: 0; +} + +/* Responsive design - stack on smaller screens */ +@media (max-width: 900px) { + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide-content { + grid-template-columns: 1fr; + grid-template-areas: + "text" + "media"; + gap: 24px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content { + padding-right: 0; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + min-height: 200px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-slide { + padding: 0 16px; + } + + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-text-content, + .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-media-content { + min-height: unset; + height: auto; + } +} + +/* Animation for slide transitions */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide.animatable .step-slides-container { + transition: transform 0.25s ease; +} + +/* Low motion preference */ +.monaco-workbench.reduce-motion .part.editor > .content .gettingStartedContainer.newSlide .step-slides-container { + transition: none; +} + +/* Hide moreText and prev-button in newSlide scenarios */ +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .moreText, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .prev-button, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .prev-button.button-link, +.monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .gettingStartedSlideDetails .prev-button { + display: none; +} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index 9f54e5df21e..7a7144fc1b1 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -132,7 +132,7 @@ export class StartupPageRunnerContribution extends Disposable implements IWorkbe if (startupEditorSetting.value === 'readme') { await this.openReadme(); } else if (startupEditorSetting.value === 'welcomePage' || startupEditorSetting.value === 'welcomePageInEmptyWorkbench') { - await this.openGettingStarted(); + await this.openGettingStarted(true); } else if (startupEditorSetting.value === 'terminal') { this.commandService.executeCommand(TerminalCommandId.CreateTerminalEditor); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index ce3d40e2eb1..c370ab068a9 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -174,17 +174,6 @@ export const startEntries: GettingStartedStartEntryContent = [ command: 'command:remoteHub.openRepository', } }, - { - id: 'topLevelShowWalkthroughs', - title: localize('gettingStarted.topLevelShowWalkthroughs.title', "Open a Walkthrough..."), - description: localize('gettingStarted.topLevelShowWalkthroughs.description', "View a walkthrough on the editor or an extension"), - icon: Codicon.checklist, - when: 'allWalkthroughsHidden', - content: { - type: 'startEntry', - command: 'command:welcome.showAllWalkthroughs', - } - }, { id: 'topLevelRemoteOpen', title: localize('gettingStarted.topLevelRemoteOpen.title', "Connect to..."), @@ -207,6 +196,17 @@ export const startEntries: GettingStartedStartEntryContent = [ command: 'command:workbench.action.remote.showWebStartEntryActions', } }, + { + id: 'topLevelNewWorkspaceChat', + title: localize('gettingStarted.newWorkspaceChat.title', "New Workspace with Copilot..."), + description: localize('gettingStarted.newWorkspaceChat.description', "Create a new workspace with Copilot"), + icon: Codicon.copilot, + when: '!isWeb && !chatSetupHidden', + content: { + type: 'startEntry', + command: 'command:welcome.newWorkspaceChat', + } + }, ]; const Button = (title: string, href: string) => `[${title}](${href})`; diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index bd2e3c3959c..c192b3a38a7 100644 --- a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -17,7 +17,7 @@ import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } f import { ILabelService } from '../../../../platform/label/common/label.js'; import { IInputOptions, IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkspaceContextService, IWorkspaceFolderData, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; import { IEditorService } from '../../editor/common/editorService.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; @@ -44,7 +44,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR editorService: IEditorService, private readonly configurationService: IConfigurationService, private readonly commandService: ICommandService, - private readonly workspaceContextService: IWorkspaceContextService, + workspaceContextService: IWorkspaceContextService, private readonly quickInputService: IQuickInputService, private readonly labelService: ILabelService, private readonly pathService: IPathService, @@ -144,10 +144,6 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR } override async resolveWithInteractionReplace(folder: IWorkspaceFolderData | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise { - // First resolve any non-interactive variables and any contributed variables - config = await this.resolveAsync(folder, config); - - // Then resolve input variables in the order in which they are encountered const parsed = ConfigurationResolverExpression.parse(config); await this.resolveWithInteraction(folder, parsed, section, variables, target); @@ -180,9 +176,14 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR else if (this._contributedVariables.has(variable.inner)) { result = { value: await this._contributedVariables.get(variable.inner)!() }; } - // Not something we can handle else { - continue; + // Fallback to parent evaluation + const resolvedValue = await this.evaluateSingleVariable(variable, folder?.uri); + if (resolvedValue === undefined) { + // Not something we can handle + continue; + } + result = typeof resolvedValue === 'string' ? { value: resolvedValue } : resolvedValue; } if (result === undefined) { @@ -197,7 +198,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR } private async resolveInputs(folder: IWorkspaceFolderData | undefined, section: string, target?: ConfigurationTarget): Promise { - if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !section) { + if (!section) { return undefined; } @@ -276,7 +277,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (typeof resolvedInput === 'string') { this.storeInputLru(defaultValueMap.set(defaultValueKey, resolvedInput)); } - return resolvedInput ? { value: resolvedInput as string, input: info } : undefined; + return resolvedInput !== undefined ? { value: resolvedInput as string, input: info } : undefined; }); } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts index 19f73d8838e..1cabb263d78 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts @@ -104,11 +104,10 @@ export class ConfigurationResolverExpression implements IConfigurationResolve private applyPlatformSpecificKeys() { const config = this.root as any; // already cloned by ctor, safe to change const key = isWindows ? 'windows' : isMacintosh ? 'osx' : isLinux ? 'linux' : undefined; - if (key === undefined || !config || typeof config !== 'object' || !config.hasOwnProperty(key)) { - return; - } - Object.keys(config[key]).forEach(k => config[k] = config[key][k]); + if (key && config && typeof config === 'object' && config.hasOwnProperty(key)) { + Object.keys(config[key]).forEach(k => config[k] = config[key][k]); + } delete config.windows; delete config.osx; diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 3f54c7ddb46..3b2c9d392a8 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -69,13 +69,9 @@ export abstract class AbstractVariableResolverService implements IConfigurationR public async resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolderData | undefined, value: string): Promise { const expr = ConfigurationResolverExpression.parse(value); - const env: Environment = { - env: this.prepareEnv(environment), - userHome: undefined - }; for (const replacement of expr.unresolved()) { - const resolvedValue = await this.evaluateSingleVariable(env, replacement, folder?.uri); + const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri, environment); if (resolvedValue !== undefined) { expr.resolve(replacement, String(resolvedValue)); } @@ -87,13 +83,8 @@ export abstract class AbstractVariableResolverService implements IConfigurationR public async resolveAsync(folder: IWorkspaceFolderData | undefined, config: T): Promise ? R : T> { const expr = ConfigurationResolverExpression.parse(config); - const environment: Environment = { - env: await this._envVariablesPromise, - userHome: await this._userHomePromise - }; - for (const replacement of expr.unresolved()) { - const resolvedValue = await this.evaluateSingleVariable(environment, replacement, folder?.uri); + const resolvedValue = await this.evaluateSingleVariable(replacement, folder?.uri); if (resolvedValue !== undefined) { expr.resolve(replacement, String(resolvedValue)); } @@ -123,7 +114,14 @@ export abstract class AbstractVariableResolverService implements IConfigurationR return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath; } - private async evaluateSingleVariable(environment: Environment, replacement: Replacement, folderUri: uri | undefined, commandValueMapping?: IStringDictionary): Promise { + protected async evaluateSingleVariable(replacement: Replacement, folderUri: uri | undefined, processEnvironment?: IProcessEnvironment, commandValueMapping?: IStringDictionary): Promise { + + + const environment: Environment = { + env: (processEnvironment !== undefined) ? this.prepareEnv(processEnvironment) : await this._envVariablesPromise, + userHome: (processEnvironment !== undefined) ? undefined : await this._userHomePromise + }; + const { name: variable, arg: argument } = replacement; // common error handling for all variables that require an open editor diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index fc4403ef079..68d24b67cf9 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -101,6 +101,26 @@ suite('Configuration Resolver Service', () => { } }); + test('does not preserve platform config even when not matched', async () => { + const obj = { + program: 'osx.sh', + windows: { + program: 'windows.exe' + }, + linux: { + program: 'linux.sh' + } + }; + const config: any = await configurationResolverService!.resolveAsync(workspace, obj); + + const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined; + + assert.strictEqual(config.windows, undefined); + assert.strictEqual(config.osx, undefined); + assert.strictEqual(config.linux, undefined); + assert.strictEqual(config.program, expected); + }); + test('apples platform specific config', async () => { const expected = isWindows ? 'windows.exe' : isMacintosh ? 'osx.sh' : isLinux ? 'linux.sh' : undefined; const obj = { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index b3a64ddfbda..3b68ec6f3fe 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IResourceEditorInput, IEditorOptions, EditorActivation, IResourceEditorInputIdentifier, ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, EditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, IFindEditorOptions, isResourceMergeEditorInput, IEditorWillOpenEvent, IEditorControl } from '../../../common/editor.js'; +import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, EditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, IFindEditorOptions, isResourceMergeEditorInput, IEditorWillOpenEvent, IEditorControl, ITextResourceDiffEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; import { ResourceMap, ResourceSet } from '../../../../base/common/map.js'; @@ -530,6 +530,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { openEditor(editor: IUntypedEditorInput, group?: PreferredGroup): Promise; openEditor(editor: IResourceEditorInput, group?: PreferredGroup): Promise; openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: PreferredGroup): Promise; + openEditor(editor: ITextResourceDiffEditorInput, group?: PreferredGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: PreferredGroup): Promise; openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise; async openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise { diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 3c406fcf104..2847f2d4afd 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -5,7 +5,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IResourceEditorInput, IEditorOptions, IResourceEditorInputIdentifier, ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, IFindEditorOptions, IEditorWillOpenEvent } from '../../../common/editor.js'; +import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, IFindEditorOptions, IEditorWillOpenEvent, ITextResourceDiffEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { Event } from '../../../../base/common/event.js'; import { IEditor, IDiffEditor } from '../../../../editor/common/editorCommon.js'; @@ -260,7 +260,7 @@ export interface IEditorService { */ openEditor(editor: IResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; - openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; + openEditor(editor: ITextResourceDiffEditorInput | IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; openEditor(editor: IUntypedEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): Promise; /** diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index a61a1c3b6f0..24eeff23dc3 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions, INotificationSource, INotificationSourceFilter, isNotificationSource, IStatusHandle } from '../../../../platform/notification/common/notification.js'; import { NotificationsModel, ChoiceAction, NotificationChangeType } from '../../../common/notifications.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IAction, Action } from '../../../../base/common/actions.js'; @@ -346,7 +346,7 @@ export class NotificationService extends Disposable implements INotificationServ return handle; } - status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable { + status(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle { return this.model.showStatusMessage(message, options); } } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 88619390030..923021734e0 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -17,7 +17,7 @@ import { ITelemetryData } from '../../../../platform/telemetry/common/telemetry. import { Event } from '../../../../base/common/event.js'; import * as paths from '../../../../base/common/path.js'; import { isCancellationError } from '../../../../base/common/errors.js'; -import { GlobPattern, TextSearchCompleteMessageType } from './searchExtTypes.js'; +import { AISearchKeyword, GlobPattern, TextSearchCompleteMessageType } from './searchExtTypes.js'; import { isThenable } from '../../../../base/common/async.js'; import { ResourceSet } from '../../../../base/common/map.js'; @@ -263,6 +263,7 @@ export interface ISearchCompleteStats { export interface ISearchComplete extends ISearchCompleteStats { results: IFileMatch[]; exit?: SearchCompletionExitCode; + aiKeywords?: AISearchKeyword[]; } export const enum SearchCompletionExitCode { diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index decb405a400..595b0014095 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -315,11 +315,28 @@ export class TextSearchContext2 { public lineNumber: number) { } } +/** +/** + * Keyword suggestion for AI search. + */ +export class AISearchKeyword { + /** + * @param keyword The keyword associated with the search. + */ + constructor(public keyword: string) { } +} + /** * A result payload for a text search, pertaining to matches within a single file. */ export type TextSearchResult2 = TextSearchMatch2 | TextSearchContext2; +/** + * A result payload for an AI search. + * This can be a {@link TextSearchMatch2 match} or a {@link AISearchKeyword keyword}. + * The result can be a match or a keyword. +*/ +export type AISearchResult = TextSearchResult2 | AISearchKeyword; /** * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions. diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index b0981b9891c..f52bcd99ed2 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -213,7 +213,8 @@ export class SearchService extends Disposable implements ISearchService { limitHit: completes[0] && completes[0].limitHit, stats: completes[0].stats, messages: arrays.coalesce(completes.flatMap(i => i.messages)).filter(arrays.uniqueFilter(message => message.type + message.text + message.trusted)), - results: completes.flatMap((c: ISearchComplete) => c.results) + results: completes.flatMap((c: ISearchComplete) => c.results), + aiKeywords: completes.flatMap((c: ISearchComplete) => c.aiKeywords).filter(keyword => keyword !== undefined), }; })(); diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 59a10ed9024..92571aef883 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -12,7 +12,7 @@ import * as resources from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { FolderQuerySearchTree } from './folderQuerySearchTree.js'; import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, excludeToGlobPattern, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider, ISearchRange, DEFAULT_TEXT_SEARCH_PREVIEW_OPTIONS } from './search.js'; -import { TextSearchComplete2, TextSearchMatch2, TextSearchProviderFolderOptions, TextSearchProvider2, TextSearchProviderOptions, TextSearchQuery2, TextSearchResult2, AITextSearchProvider } from './searchExtTypes.js'; +import { TextSearchComplete2, TextSearchMatch2, TextSearchProviderFolderOptions, TextSearchProvider2, TextSearchProviderOptions, TextSearchQuery2, TextSearchResult2, AITextSearchProvider, AISearchResult, AISearchKeyword } from './searchExtTypes.js'; export interface IFileUtils { readdir: (resource: URI) => Promise; @@ -46,7 +46,7 @@ export class TextSearchManager { return this.queryProviderPair.query; } - search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { + search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { const folderQueries = this.query.folderQueries || []; const tokenSource = new CancellationTokenSource(token); @@ -55,6 +55,10 @@ export class TextSearchManager { let isCanceled = false; const onResult = (result: TextSearchResult2, folderIdx: number) => { + if (result instanceof AISearchKeyword) { + // Already processed by the callback. + return; + } if (isCanceled) { return; } @@ -80,7 +84,7 @@ export class TextSearchManager { }; // For each root folder - this.doSearch(folderQueries, onResult, tokenSource.token).then(result => { + this.doSearch(folderQueries, onResult, tokenSource.token, onKeywordResult).then(result => { tokenSource.dispose(); this.collector!.flush(); @@ -121,7 +125,7 @@ export class TextSearchManager { return new TextSearchMatch2(result.uri, result.ranges.slice(0, size), result.previewText); } - private async doSearch(folderQueries: IFolderQuery[], onResult: (result: TextSearchResult2, folderIdx: number) => void, token: CancellationToken): Promise { + private async doSearch(folderQueries: IFolderQuery[], onResult: (result: TextSearchResult2, folderIdx: number) => void, token: CancellationToken, onKeywordResult?: (keyword: AISearchKeyword) => void): Promise { const folderMappings: FolderQuerySearchTree = new FolderQuerySearchTree( folderQueries, (fq, i) => { @@ -133,31 +137,34 @@ export class TextSearchManager { const testingPs: Promise[] = []; const progress = { - report: (result: TextSearchResult2) => { + report: (result: TextSearchResult2 | AISearchResult) => { + if (result instanceof AISearchKeyword) { + onKeywordResult?.(result); + } else { + if (result.uri === undefined) { + throw Error('Text search result URI is undefined. Please check provider implementation.'); + } + const folderQuery = folderMappings.findQueryFragmentAwareSubstr(result.uri)!; + const hasSibling = folderQuery.folder.scheme === Schemas.file ? + hasSiblingPromiseFn(() => { + return this.fileUtils.readdir(resources.dirname(result.uri)); + }) : + undefined; - if (result.uri === undefined) { - throw Error('Text search result URI is undefined. Please check provider implementation.'); - } - const folderQuery = folderMappings.findQueryFragmentAwareSubstr(result.uri)!; - const hasSibling = folderQuery.folder.scheme === Schemas.file ? - hasSiblingPromiseFn(() => { - return this.fileUtils.readdir(resources.dirname(result.uri)); - }) : - undefined; - - const relativePath = resources.relativePath(folderQuery.folder, result.uri); - if (relativePath) { - // This method is only async when the exclude contains sibling clauses - const included = folderQuery.queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); - if (isThenable(included)) { - testingPs.push( - included.then(isIncluded => { - if (isIncluded) { - onResult(result, folderQuery.folderIdx); - } - })); - } else if (included) { - onResult(result, folderQuery.folderIdx); + const relativePath = resources.relativePath(folderQuery.folder, result.uri); + if (relativePath) { + // This method is only async when the exclude contains sibling clauses + const included = folderQuery.queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); + if (isThenable(included)) { + testingPs.push( + included.then(isIncluded => { + if (isIncluded) { + onResult(result, folderQuery.folderIdx); + } + })); + } else if (included) { + onResult(result, folderQuery.folderIdx); + } } } } diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 510a48dc03c..80cb08bf05f 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -275,7 +275,7 @@ suite('Notifications', () => { assert.strictEqual(model.statusMessage!.message, 'Hello World'); assert.strictEqual(lastStatusMessageEvent.item.message, model.statusMessage!.message); assert.strictEqual(lastStatusMessageEvent.kind, StatusMessageChangeType.ADD); - disposable.dispose(); + disposable.close(); assert.ok(!model.statusMessage); assert.strictEqual(lastStatusMessageEvent.kind, StatusMessageChangeType.REMOVE); @@ -284,10 +284,10 @@ suite('Notifications', () => { assert.strictEqual(model.statusMessage!.message, 'Hello World 3'); - disposable2.dispose(); + disposable2.close(); assert.strictEqual(model.statusMessage!.message, 'Hello World 3'); - disposable3.dispose(); + disposable3.close(); assert.ok(!model.statusMessage); item2DuplicateHandle.close(); diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index e2fce42aacb..d85284ac286 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -14380,9 +14380,26 @@ declare module 'vscode' { * can properly apply. Do not use this method to decode content * in chunks, as that may lead to incorrect results. * - * If no encoding is provided, will try to pick an encoding based - * on settings and the content of the buffer (for example byte order - * marks). + * Will pick an encoding based on settings and the content of the + * buffer (for example byte order marks). + * + * *Note* that if you decode content that is unsupported by the + * encoding, the result may contain substitution characters as + * appropriate. + * + * @throws This method will throw an error when the content is binary. + * + * @param content The text content to decode as a `Uint8Array`. + * @returns A thenable that resolves to the decoded `string`. + */ + export function decode(content: Uint8Array): Thenable; + + /** + * Decodes the content from a `Uint8Array` to a `string` using the + * provided encoding. You MUST provide the entire content at once + * to ensure that the encoding can properly apply. Do not use this + * method to decode content in chunks, as that may lead to incorrect + * results. * * *Note* that if you decode content that is unsupported by the * encoding, the result may contain substitution characters as @@ -14399,6 +14416,8 @@ declare module 'vscode' { * Allows to explicitly pick the encoding to use. * See {@link TextDocument.encoding} for more information * about valid values for encoding. + * Using an unsupported encoding will fallback to the + * default configured encoding. */ readonly encoding: string; }): Thenable; @@ -14428,14 +14447,22 @@ declare module 'vscode' { * is used to figure out the encoding related configuration * for the file if any. */ - readonly uri: Uri | undefined; + readonly uri: Uri; }): Thenable; /** * Encodes the content of a `string` to a `Uint8Array`. * - * If no encoding is provided, will try to pick an encoding based - * on settings. + * Will pick an encoding based on settings. + * + * @param content The content to decode as a `string`. + * @returns A thenable that resolves to the encoded `Uint8Array`. + */ + export function encode(content: string): Thenable; + + /** + * Encodes the content of a `string` to a `Uint8Array` using the + * provided encoding. * * @param content The content to decode as a `string`. * @param options Additional context for picking the encoding. @@ -14446,6 +14473,8 @@ declare module 'vscode' { * Allows to explicitly pick the encoding to use. * See {@link TextDocument.encoding} for more information * about valid values for encoding. + * Using an unsupported encoding will fallback to the + * default configured encoding. */ readonly encoding: string; }): Thenable; @@ -14465,7 +14494,7 @@ declare module 'vscode' { * is used to figure out the encoding related configuration * for the file if any. */ - readonly uri: Uri | undefined; + readonly uri: Uri; }): Thenable; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 9f242727688..6dce17bb29e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -79,7 +79,7 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart; export class ChatResponseWarningPart { value: MarkdownString; @@ -159,6 +159,13 @@ declare module 'vscode' { resolve?(token: CancellationToken): Thenable; } + export class ChatResponseExtensionsPart { + + readonly extensions: string[]; + + constructor(extensions: string[]); + } + export interface ChatResponseStream { /** @@ -405,7 +412,7 @@ declare module 'vscode' { } export namespace lm { - export function fileIsIgnored(uri: Uri, token: CancellationToken): Thenable; + export function fileIsIgnored(uri: Uri, token?: CancellationToken): Thenable; } export interface ChatVariableValue { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 65f6729e38e..43010b7300c 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 7 +// version: 8 declare module 'vscode' { @@ -77,6 +77,67 @@ declare module 'vscode' { * or terminal. Will be `undefined` for the chat panel. */ readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; + + /** + * Events for edited files in this session collected since the last request. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + } + + export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, + } + + export interface ChatRequestEditedFileEvent { + readonly uri: Uri; + readonly eventKind: ChatRequestEditedFileEventKind; + } + + /** + * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. + */ + export class ChatRequestTurn2 { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * The list of tools were attached to this request. + */ + readonly toolReferences: readonly ChatLanguageModelToolReference[]; + + /** + * Events for edited files in this session collected between the previous request and this one. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); } export interface ChatParticipant { diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 7da03756940..f3edb530291 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -40,6 +40,11 @@ declare module 'vscode' { */ readonly family: string; + /** + * An optional, human-readable description of the language model. + */ + readonly description?: string; + /** * Opaque version string of the model. This is defined by the extension contributing the language model * and subject to change while the identifier is stable. diff --git a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts index 622c6cb2ce6..65821c87e4f 100644 --- a/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 1 + declare module 'vscode' { export interface LanguageModelChat { @@ -21,7 +23,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - static User(content: string | Array, name?: string): LanguageModelChatMessage2; + static User(content: string | Array, name?: string): LanguageModelChatMessage2; /** * Utility to create a new assistant message. @@ -29,7 +31,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - static Assistant(content: string | Array, name?: string): LanguageModelChatMessage2; + static Assistant(content: string | Array, name?: string): LanguageModelChatMessage2; /** * The role of this message. @@ -40,7 +42,7 @@ declare module 'vscode' { * A string or heterogeneous array of things that a message can contain as content. Some parts may be message-type * specific for some models. */ - content: Array; + content: Array; /** * The optional name of a user for this message. @@ -54,7 +56,7 @@ declare module 'vscode' { * @param content The content of the message. * @param name The optional name of a user for the message. */ - constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); + constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); } /** @@ -96,6 +98,30 @@ declare module 'vscode' { data: Uint8Array; } + /** + * Tagging onto this proposal, because otherwise managing two different extensions of LangaugeModelChatMessage could be confusing. + * A language model response part containing arbitrary model-specific data, returned from a {@link LanguageModelChatResponse}. + * TODO@API naming, looking at LanguageModelChatRequestOptions.modelOptions, but LanguageModelModelData is not very good. + * LanguageModelOpaqueData from prompt-tsx? + */ + export class LanguageModelExtraDataPart { + /** + * The type of data. The allowed values and data types here are model-specific. + */ + kind: string; + + /** + * Extra model-specific data. + */ + data: any; + + /** + * Construct an extra data part with the given content. + * @param value The image content of the part. + */ + constructor(kind: string, data: any); + } + /** * The result of a tool call. This is the counterpart of a {@link LanguageModelToolCallPart tool call} and diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 50249caa8a9..b119fc57672 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -75,6 +75,7 @@ declare module 'vscode' { readonly codeBlocks: { code: string; resource: Uri; markdownBeforeBlock?: string }[]; readonly location?: string; readonly chatRequestId?: string; + readonly chatRequestModel?: string; } export interface MappedEditsResponseStream { diff --git a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts index 28a9c7b2f36..7e7e50e4d25 100644 --- a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts @@ -6,37 +6,129 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/243522 + /** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and listening to its stdin and stdout streams. + */ export class McpStdioServerDefinition { - + /** + * The human-readable name of the server. + */ label: string; + /** + * The working directory used to start the server. + */ cwd?: Uri; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ command: string; - args: readonly string[]; + /** + * Additional command-line arguments passed to the server. + */ + args: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables. + */ env: Record; - constructor(label: string, command: string, args: string[], env: { [key: string]: string }); + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param command The command used to start the server. + * @param args Additional command-line arguments passed to the server. + * @param env Optional additional environment information for the server. + * @param version Optional version identification for the server. + */ + constructor(label: string, command: string, args?: string[], env?: Record, version?: string); } - export class McpSSEServerDefinition { - + /** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ + export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ label: string; + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ uri: Uri; - headers: [string, string][]; + /** + * Optional additional heads included with each request to the server. + */ + headers: Record; - constructor(label: string, uri: Uri); + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: Uri, headers?: Record, version?: string); } - export type McpServerDefinition = McpStdioServerDefinition | McpSSEServerDefinition; - - export interface McpConfigurationProvider { + export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + /** + * A type that can provide server configurations. This may only be used in + * conjunction with `contributes.modelContextServerCollections` in the + * extension's package.json. + * + * To allow the editor to cache available servers, extensions should register + * this before `activate()` resolves. + */ + export interface McpConfigurationProvider { + /** + * Optional event fired to signal that the set of available servers has changed. + */ onDidChange?: Event; - provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + /** + * Provides available MCP servers. The editor will call this method eagerly + * to ensure the availability of servers for the language model, and so + * extensions should not take actions which would require user + * interaction, such as authentication. + * + * @param token A cancellation token. + * @returns An array of MCP available MCP servers + */ + provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + /** + * This function will be called when the editor needs to start MCP server. + * At this point, the extension may take any actions which may require user + * interaction, such as authentication. + * + * The extension may return undefined on error to indicate that the server + * should not be started. + * + * @param server The MCP server to resolve + * @param token A cancellation token. + * @returns The given, resolved server or thenable that resolves to such. + */ + resolveMcpServerDefinition?(server: T, token: CancellationToken): ProviderResult; } namespace lm { diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 674c1ae279d..5c83a2d3557 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -11,6 +11,10 @@ declare module 'vscode' { export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, label: string, rootUri?: Uri): Disposable; } + export interface SourceControl { + secondaryQuickDiffProvider?: QuickDiffProvider; + } + export interface QuickDiffProvider { label?: string; readonly visible?: boolean; diff --git a/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts b/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts index 146b76b5fa5..2bcb81299b8 100644 --- a/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts +++ b/src/vscode-dts/vscode.proposed.textSearchProvider2.d.ts @@ -237,12 +237,34 @@ declare module 'vscode' { lineNumber: number; } + /** + * Keyword suggestion for AI search. + */ + export class AISearchKeyword { + /** + * @param keyword The keyword associated with the search. + */ + constructor(keyword: string); + + /** + * The keyword associated with the search. + */ + keyword: string; + } + /** * A result payload for a text search, pertaining to {@link TextSearchMatch2 matches} * and its associated {@link TextSearchContext2 context} within a single file. */ export type TextSearchResult2 = TextSearchMatch2 | TextSearchContext2; + /** + * A result payload for an AI search. + * This can be a {@link TextSearchMatch2 match} or a {@link AISearchKeyword keyword}. + * The result can be a match or a keyword. + */ + export type AISearchResult = TextSearchResult2 | AISearchKeyword; + /** * A TextSearchProvider provides search results for text results inside files in the workspace. */ @@ -255,7 +277,7 @@ declare module 'vscode' { * These results can be direct matches, or context that surrounds matches. * @param token A cancellation token. */ - provideTextSearchResults(query: TextSearchQuery2, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult; + provideTextSearchResults(query: TextSearchQuery2, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult; } export namespace workspace {